feat: research agent

This commit is contained in:
2026-07-19 19:36:49 +02:00
parent 8eeac8f76f
commit a52ebabf7f
8 changed files with 117 additions and 3 deletions
+4 -1
View File
@@ -7,8 +7,11 @@ AGENTCI_PLAN_MODEL=gpt-5.6-sol
AGENTCI_PLAN_REASONING=medium AGENTCI_PLAN_REASONING=medium
AGENTCI_IMPLEMENT_MODEL=gpt-5.6-sol AGENTCI_IMPLEMENT_MODEL=gpt-5.6-sol
AGENTCI_IMPLEMENT_REASONING=high AGENTCI_IMPLEMENT_REASONING=high
AGENTCI_RESEARCH_MODEL=gpt-5.6-luna
AGENTCI_RESEARCH_REASONING=high
# Optional; Context7 works without a key at lower rate limits.
AGENTCI_CONTEXT7_API_KEY=
AGENTCI_PLAN_REVIEW_ROUNDS=4 AGENTCI_PLAN_REVIEW_ROUNDS=4
AGENTCI_IMPLEMENT_REVIEW_ROUNDS=3 AGENTCI_IMPLEMENT_REVIEW_ROUNDS=3
AGENTCI_TURN_TIMEOUT_SECONDS=3600 AGENTCI_TURN_TIMEOUT_SECONDS=3600
CODEX_VERSION=0.144.6 CODEX_VERSION=0.144.6
+9
View File
@@ -54,6 +54,15 @@ turn timeout use `AGENTCI_` environment variables. Defaults are shown in
`.env.example`. Gitea credentials and webhook secrets are intentionally `.env.example`. Gitea credentials and webhook secrets are intentionally
file-based Compose secrets. file-based Compose secrets.
Every planning and implementation session can delegate external research to a
read-only `research` subagent. It defaults to `gpt-5.6-luna` with high reasoning
and has public network access, live web search, Context7 documentation lookup,
and `gh_grep` public GitHub code search. Configure its model and effort with
`AGENTCI_RESEARCH_MODEL` and `AGENTCI_RESEARCH_REASONING`. Context7 works
without authentication at lower rate limits; set the optional
`AGENTCI_CONTEXT7_API_KEY` for authenticated usage. The key is passed only to
Codex's Context7 MCP transport and is excluded from agent shell environments.
Planning/review commands can only read their workflow clone and have no shell Planning/review commands can only read their workflow clone and have no shell
network access. Implementation/fix commands can edit the clone but cannot network access. Implementation/fix commands can edit the clone but cannot
modify `.git`; they can reach public internet destinations while private and modify `.git`; they can reach public internet destinations while private and
+28
View File
@@ -3,6 +3,15 @@ approval_policy = "never"
check_for_update_on_startup = false check_for_update_on_startup = false
web_search = "disabled" web_search = "disabled"
default_permissions = "agentci-read" default_permissions = "agentci-read"
developer_instructions = """
A custom research subagent named `research` is available in every workflow. Delegate focused
external research to it when current documentation, web evidence, or public code examples would
materially improve the plan or implementation. Keep repository analysis and edits in the parent.
"""
[agents]
max_threads = 4
max_depth = 1
[shell_environment_policy] [shell_environment_policy]
inherit = "core" inherit = "core"
@@ -38,3 +47,22 @@ allow_local_binding = false
[permissions.agentci-write.network.domains] [permissions.agentci-write.network.domains]
"*" = "allow" "*" = "allow"
[permissions.agentci-research]
description = "Read a workflow repository and research public internet sources without editing."
[permissions.agentci-research.filesystem]
":minimal" = "read"
glob_scan_max_depth = 5
[permissions.agentci-research.filesystem.":workspace_roots"]
"." = "read"
".git" = "read"
"**/*.env" = "deny"
[permissions.agentci-research.network]
enabled = true
allow_local_binding = false
[permissions.agentci-research.network.domains]
"*" = "allow"
+3 -1
View File
@@ -14,6 +14,9 @@ services:
AGENTCI_PLAN_REASONING: ${AGENTCI_PLAN_REASONING:-medium} AGENTCI_PLAN_REASONING: ${AGENTCI_PLAN_REASONING:-medium}
AGENTCI_IMPLEMENT_MODEL: ${AGENTCI_IMPLEMENT_MODEL:-gpt-5.6-sol} AGENTCI_IMPLEMENT_MODEL: ${AGENTCI_IMPLEMENT_MODEL:-gpt-5.6-sol}
AGENTCI_IMPLEMENT_REASONING: ${AGENTCI_IMPLEMENT_REASONING:-high} AGENTCI_IMPLEMENT_REASONING: ${AGENTCI_IMPLEMENT_REASONING:-high}
AGENTCI_RESEARCH_MODEL: ${AGENTCI_RESEARCH_MODEL:-gpt-5.6-luna}
AGENTCI_RESEARCH_REASONING: ${AGENTCI_RESEARCH_REASONING:-high}
AGENTCI_CONTEXT7_API_KEY: ${AGENTCI_CONTEXT7_API_KEY:-}
AGENTCI_PLAN_REVIEW_ROUNDS: ${AGENTCI_PLAN_REVIEW_ROUNDS:-4} AGENTCI_PLAN_REVIEW_ROUNDS: ${AGENTCI_PLAN_REVIEW_ROUNDS:-4}
AGENTCI_IMPLEMENT_REVIEW_ROUNDS: ${AGENTCI_IMPLEMENT_REVIEW_ROUNDS:-3} AGENTCI_IMPLEMENT_REVIEW_ROUNDS: ${AGENTCI_IMPLEMENT_REVIEW_ROUNDS:-3}
AGENTCI_TURN_TIMEOUT_SECONDS: ${AGENTCI_TURN_TIMEOUT_SECONDS:-3600} AGENTCI_TURN_TIMEOUT_SECONDS: ${AGENTCI_TURN_TIMEOUT_SECONDS:-3600}
@@ -42,4 +45,3 @@ networks:
gitea: gitea:
external: true external: true
name: ${GITEA_NETWORK:-gitea} name: ${GITEA_NETWORK:-gitea}
+35
View File
@@ -26,10 +26,15 @@ class CodexClient:
codex_home: Path, codex_home: Path,
schemas_dir: Path, schemas_dir: Path,
timeout_seconds: int, timeout_seconds: int,
research_model: str,
research_reasoning: str,
context7_api_key: str | None,
) -> None: ) -> None:
self.codex_home = codex_home self.codex_home = codex_home
self.schemas_dir = schemas_dir self.schemas_dir = schemas_dir
self.timeout_seconds = timeout_seconds self.timeout_seconds = timeout_seconds
self.context7_api_key = context7_api_key
self._write_research_agent(research_model, research_reasoning)
async def login_ready(self) -> bool: async def login_ready(self) -> bool:
try: try:
@@ -174,8 +179,38 @@ class CodexClient:
allowed = {"PATH", "LANG", "LC_ALL", "SSL_CERT_FILE", "CODEX_CA_CERTIFICATE"} allowed = {"PATH", "LANG", "LC_ALL", "SSL_CERT_FILE", "CODEX_CA_CERTIFICATE"}
environment = {key: value for key, value in os.environ.items() if key in allowed} environment = {key: value for key, value in os.environ.items() if key in allowed}
environment["CODEX_HOME"] = str(self.codex_home) environment["CODEX_HOME"] = str(self.codex_home)
if self.context7_api_key:
environment["CONTEXT7_API_KEY"] = self.context7_api_key
return environment return environment
def _write_research_agent(self, model: str, reasoning: str) -> None:
agents_dir = self.codex_home / "agents"
agents_dir.mkdir(parents=True, exist_ok=True)
agent = f'''name = "research"
description = "Research specialist for current docs, web evidence, and public code examples."
model = {json.dumps(model)}
model_reasoning_effort = {json.dumps(reasoning)}
default_permissions = "agentci-research"
web_search = "live"
developer_instructions = """
Research external, current, or unfamiliar technical facts for the parent agent.
Use Context7 for library documentation, gh_grep for real public-code examples, and web search for
primary sources or broader verification. Prefer authoritative sources, report links, distinguish
facts from inference, and return a concise evidence-focused summary. You may inspect the workspace
but must not modify it. Never include secrets or proprietary source in external queries.
"""
[mcp_servers.context7]
url = "https://mcp.context7.com/mcp"
[mcp_servers.context7.env_http_headers]
CONTEXT7_API_KEY = "CONTEXT7_API_KEY"
[mcp_servers.gh_grep]
url = "https://mcp.grep.app"
'''
(agents_dir / "research.toml").write_text(agent)
def _session_id(output: str) -> str | None: def _session_id(output: str) -> str | None:
for line in output.splitlines(): for line in output.splitlines():
+4 -1
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from functools import cached_property from functools import cached_property
from pathlib import Path from pathlib import Path
from pydantic import Field, field_validator from pydantic import Field, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -30,6 +30,9 @@ class Settings(BaseSettings):
plan_reasoning: str = "medium" plan_reasoning: str = "medium"
implement_model: str = "gpt-5.6-sol" implement_model: str = "gpt-5.6-sol"
implement_reasoning: str = "high" implement_reasoning: str = "high"
research_model: str = "gpt-5.6-luna"
research_reasoning: str = "high"
context7_api_key: SecretStr | None = None
plan_review_rounds: int = Field(default=4, ge=1, le=20) plan_review_rounds: int = Field(default=4, ge=1, le=20)
implement_review_rounds: int = Field(default=3, ge=1, le=20) implement_review_rounds: int = Field(default=3, ge=1, le=20)
turn_timeout_seconds: int = Field(default=3600, ge=60) turn_timeout_seconds: int = Field(default=3600, ge=60)
+7
View File
@@ -53,6 +53,13 @@ async def build_container(settings: Settings) -> Container:
codex_home=settings.codex_home, codex_home=settings.codex_home,
schemas_dir=package_dir / "prompts" / "schemas", schemas_dir=package_dir / "prompts" / "schemas",
timeout_seconds=settings.turn_timeout_seconds, timeout_seconds=settings.turn_timeout_seconds,
research_model=settings.research_model,
research_reasoning=settings.research_reasoning,
context7_api_key=(
settings.context7_api_key.get_secret_value()
if settings.context7_api_key is not None
else None
),
) )
prompts = PromptLibrary() prompts = PromptLibrary()
context = ContextBuilder(gitea, storage) context = ContextBuilder(gitea, storage)
+27
View File
@@ -1,3 +1,5 @@
import tomllib
from agentci.adapters.codex import CodexClient, _session_id from agentci.adapters.codex import CodexClient, _session_id
@@ -21,8 +23,33 @@ def test_turns_skip_interactive_git_trust_check(tmp_path) -> None:
codex_home=tmp_path / "codex", codex_home=tmp_path / "codex",
schemas_dir=tmp_path / "schemas", schemas_dir=tmp_path / "schemas",
timeout_seconds=60, timeout_seconds=60,
research_model="gpt-5.6-luna",
research_reasoning="high",
context7_api_key=None,
) )
args = client._turn_args("model", "medium", "agentci-read", "plan.json") args = client._turn_args("model", "medium", "agentci-read", "plan.json")
assert "--skip-git-repo-check" in args assert "--skip-git-repo-check" in args
def test_configures_research_agent_and_optional_context7_key(tmp_path) -> None:
client = CodexClient(
codex_home=tmp_path / "codex",
schemas_dir=tmp_path / "schemas",
timeout_seconds=60,
research_model="research-model",
research_reasoning="high",
context7_api_key="ctx7-secret",
)
agent = (tmp_path / "codex" / "agents" / "research.toml").read_text()
parsed = tomllib.loads(agent)
assert 'name = "research"' in agent
assert parsed["model"] == "research-model"
assert parsed["model_reasoning_effort"] == "high"
assert parsed["web_search"] == "live"
assert 'url = "https://mcp.context7.com/mcp"' in agent
assert 'url = "https://mcp.grep.app"' in agent
assert "ctx7-secret" not in agent
assert client._environment()["CONTEXT7_API_KEY"] == "ctx7-secret"