feat: codegraph
This commit is contained in:
@@ -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,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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user