feat: research agent
This commit is contained in:
+4
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user