feat: codegraph

This commit is contained in:
2026-07-19 19:55:34 +02:00
parent a52ebabf7f
commit 29d12974fd
12 changed files with 203 additions and 13 deletions
+82
View File
@@ -0,0 +1,82 @@
from __future__ import annotations
import asyncio
import logging
import os
from pathlib import Path
from time import monotonic
log = logging.getLogger(__name__)
class CodeGraphError(RuntimeError):
pass
class CodeGraphClient:
async def prepare(self, workspace: Path) -> None:
self._exclude_index(workspace)
command = "sync" if (workspace / ".codegraph").is_dir() else "init"
started = monotonic()
log.info(
"CodeGraph index preparation started",
extra={"operation": f"codegraph.{command}"},
)
try:
process = await asyncio.create_subprocess_exec(
"codegraph",
command,
str(workspace),
cwd=workspace,
env={**os.environ, "CODEGRAPH_TELEMETRY": "0"},
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
_, stderr = await process.communicate()
except OSError as exc:
log.exception(
"CodeGraph could not start",
extra={
"operation": f"codegraph.{command}",
"duration_ms": _elapsed_ms(started),
},
)
raise CodeGraphError(f"Could not run CodeGraph: {exc}") from exc
if process.returncode:
detail = stderr.decode(errors="replace").strip()
log.error(
"CodeGraph index preparation failed",
extra={
"operation": f"codegraph.{command}",
"duration_ms": _elapsed_ms(started),
},
)
raise CodeGraphError(
f"codegraph {command} failed: {detail[-1000:]}"
)
log.info(
"CodeGraph index preparation completed",
extra={
"operation": f"codegraph.{command}",
"duration_ms": _elapsed_ms(started),
},
)
@staticmethod
def _exclude_index(workspace: Path) -> None:
exclude = workspace / ".git" / "info" / "exclude"
exclude.parent.mkdir(parents=True, exist_ok=True)
content = exclude.read_text() if exclude.exists() else ""
patterns = {
line.strip()
for line in content.splitlines()
if line.strip() and not line.lstrip().startswith("#")
}
if ".codegraph/" in patterns:
return
separator = "" if not content or content.endswith("\n") else "\n"
exclude.write_text(f"{content}{separator}.codegraph/\n")
def _elapsed_ms(started: float) -> int:
return round((monotonic() - started) * 1000)
+11
View File
@@ -11,6 +11,8 @@ from typing import TypeVar
from pydantic import BaseModel
from agentci.adapters.codegraph import CodeGraphClient
T = TypeVar("T", bound=BaseModel)
log = logging.getLogger(__name__)
@@ -29,11 +31,13 @@ class CodexClient:
research_model: str,
research_reasoning: str,
context7_api_key: str | None,
codegraph: CodeGraphClient | None = 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.codegraph = codegraph or CodeGraphClient()
self._write_research_agent(research_model, research_reasoning)
async def login_ready(self) -> bool:
@@ -61,6 +65,7 @@ class CodexClient:
schema_name: str,
result_type: type[T],
) -> tuple[str, T]:
await self.codegraph.prepare(workspace)
args = [
"codex",
"exec",
@@ -84,9 +89,11 @@ class CodexClient:
model: str,
reasoning: str,
permission: str,
workspace: Path,
schema_name: str,
result_type: type[T],
) -> T:
await self.codegraph.prepare(workspace)
args = [
"codex",
"exec",
@@ -198,8 +205,12 @@ Use Context7 for library documentation, gh_grep for real public-code examples, a
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.
Do not use CodeGraph; repository analysis belongs to the parent agent.
"""
[mcp_servers.codegraph]
enabled = false
[mcp_servers.context7]
url = "https://mcp.context7.com/mcp"
+2 -1
View File
@@ -40,6 +40,7 @@ class CodeReviewLoop:
model=self.deps.settings.implement_model,
reasoning=self.deps.settings.implement_reasoning,
permission="agentci-write",
workspace=workflow.workspace_path,
schema_name="agent_result.json",
result_type=AgentResult,
)
@@ -54,6 +55,7 @@ class CodeReviewLoop:
model=self.deps.settings.implement_model,
reasoning=self.deps.settings.implement_reasoning,
permission="agentci-read",
workspace=workflow.workspace_path,
schema_name="review.json",
result_type=ReviewReport,
)
@@ -74,4 +76,3 @@ def _required(value: str | None) -> str:
if value is None:
raise RuntimeError("Expected a persisted Codex session ID")
return value
+6 -9
View File
@@ -85,6 +85,7 @@ class PlanWorkflow:
model=self.deps.settings.plan_model,
reasoning=self.deps.settings.plan_reasoning,
permission="agentci-read",
workspace=workflow.workspace_path,
schema_name="discussion.json",
result_type=DiscussionReply,
)
@@ -121,6 +122,7 @@ class PlanWorkflow:
model=self.deps.settings.plan_model,
reasoning=self.deps.settings.plan_reasoning,
permission="agentci-read",
workspace=workflow.workspace_path,
schema_name="plan.json",
result_type=PlanArtifact,
)
@@ -128,11 +130,7 @@ class PlanWorkflow:
await self._finish(job, workflow, artifact, report)
async def _review_loop(
self,
job: Job,
workflow: Workflow,
context: str,
artifact: PlanArtifact,
self, job: Job, workflow: Workflow, context: str, artifact: PlanArtifact
) -> ReviewReport:
report = ReviewReport(summary="", findings=[])
for round_index in range(self.deps.settings.plan_review_rounds):
@@ -160,16 +158,14 @@ class PlanWorkflow:
model=self.deps.settings.plan_model,
reasoning=self.deps.settings.plan_reasoning,
permission="agentci-read",
workspace=workflow.workspace_path,
schema_name="plan.json",
result_type=PlanArtifact,
)
return report
async def _review(
self,
workflow: Workflow,
context: str,
artifact: PlanArtifact,
self, workflow: Workflow, context: str, artifact: PlanArtifact
) -> ReviewReport:
prompt = self.deps.prompts.render(
"plan_review", context=context, artifact=artifact.plan_markdown
@@ -181,6 +177,7 @@ class PlanWorkflow:
model=self.deps.settings.plan_model,
reasoning=self.deps.settings.plan_reasoning,
permission="agentci-read",
workspace=workflow.workspace_path,
schema_name="review.json",
result_type=ReviewReport,
)
+1
View File
@@ -54,6 +54,7 @@ class PullRequestWorkflow:
model=self.deps.settings.implement_model,
reasoning=self.deps.settings.implement_reasoning,
permission="agentci-write",
workspace=workflow.workspace_path,
schema_name="agent_result.json",
result_type=AgentResult,
)