From a52ebabf7f15e60ef45d07253705f1f5a90c1dde Mon Sep 17 00:00:00 2001 From: StanPonomarev Date: Sun, 19 Jul 2026 19:36:49 +0200 Subject: [PATCH] feat: research agent --- .env.example | 5 ++++- README.md | 9 +++++++++ codex/config.toml | 28 ++++++++++++++++++++++++++++ compose.yaml | 4 +++- src/agentci/adapters/codex.py | 35 +++++++++++++++++++++++++++++++++++ src/agentci/config.py | 5 ++++- src/agentci/container.py | 7 +++++++ tests/test_codex.py | 27 +++++++++++++++++++++++++++ 8 files changed, 117 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index e429447..f771f17 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,11 @@ AGENTCI_PLAN_MODEL=gpt-5.6-sol AGENTCI_PLAN_REASONING=medium AGENTCI_IMPLEMENT_MODEL=gpt-5.6-sol 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_IMPLEMENT_REVIEW_ROUNDS=3 AGENTCI_TURN_TIMEOUT_SECONDS=3600 CODEX_VERSION=0.144.6 - diff --git a/README.md b/README.md index 8acf9c1..b03ce70 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,15 @@ turn timeout use `AGENTCI_` environment variables. Defaults are shown in `.env.example`. Gitea credentials and webhook secrets are intentionally 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 network access. Implementation/fix commands can edit the clone but cannot modify `.git`; they can reach public internet destinations while private and diff --git a/codex/config.toml b/codex/config.toml index 7995803..0641375 100644 --- a/codex/config.toml +++ b/codex/config.toml @@ -3,6 +3,15 @@ approval_policy = "never" check_for_update_on_startup = false web_search = "disabled" 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] inherit = "core" @@ -38,3 +47,22 @@ allow_local_binding = false [permissions.agentci-write.network.domains] "*" = "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" diff --git a/compose.yaml b/compose.yaml index 2cfe443..9c47f3f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -14,6 +14,9 @@ services: AGENTCI_PLAN_REASONING: ${AGENTCI_PLAN_REASONING:-medium} AGENTCI_IMPLEMENT_MODEL: ${AGENTCI_IMPLEMENT_MODEL:-gpt-5.6-sol} 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_IMPLEMENT_REVIEW_ROUNDS: ${AGENTCI_IMPLEMENT_REVIEW_ROUNDS:-3} AGENTCI_TURN_TIMEOUT_SECONDS: ${AGENTCI_TURN_TIMEOUT_SECONDS:-3600} @@ -42,4 +45,3 @@ networks: gitea: external: true name: ${GITEA_NETWORK:-gitea} - diff --git a/src/agentci/adapters/codex.py b/src/agentci/adapters/codex.py index 2d0cb84..aa36fe0 100644 --- a/src/agentci/adapters/codex.py +++ b/src/agentci/adapters/codex.py @@ -26,10 +26,15 @@ class CodexClient: codex_home: Path, schemas_dir: Path, timeout_seconds: int, + research_model: str, + research_reasoning: str, + context7_api_key: str | None, ) -> None: self.codex_home = codex_home self.schemas_dir = schemas_dir 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: try: @@ -174,8 +179,38 @@ class CodexClient: 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["CODEX_HOME"] = str(self.codex_home) + if self.context7_api_key: + environment["CONTEXT7_API_KEY"] = self.context7_api_key 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: for line in output.splitlines(): diff --git a/src/agentci/config.py b/src/agentci/config.py index 80285ce..0fa8794 100644 --- a/src/agentci/config.py +++ b/src/agentci/config.py @@ -3,7 +3,7 @@ from __future__ import annotations from functools import cached_property from pathlib import Path -from pydantic import Field, field_validator +from pydantic import Field, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -30,6 +30,9 @@ class Settings(BaseSettings): plan_reasoning: str = "medium" implement_model: str = "gpt-5.6-sol" 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) implement_review_rounds: int = Field(default=3, ge=1, le=20) turn_timeout_seconds: int = Field(default=3600, ge=60) diff --git a/src/agentci/container.py b/src/agentci/container.py index 087e902..c8d1d05 100644 --- a/src/agentci/container.py +++ b/src/agentci/container.py @@ -53,6 +53,13 @@ async def build_container(settings: Settings) -> Container: codex_home=settings.codex_home, schemas_dir=package_dir / "prompts" / "schemas", 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() context = ContextBuilder(gitea, storage) diff --git a/tests/test_codex.py b/tests/test_codex.py index 2913ce1..c6bd640 100644 --- a/tests/test_codex.py +++ b/tests/test_codex.py @@ -1,3 +1,5 @@ +import tomllib + 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", schemas_dir=tmp_path / "schemas", 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") 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"