"""Pattern 2: a code-owned graph. Your program is the orchestrator; each node is a query().
plan ──► fan-out (one worker per file, in parallel) ──► synthesize ──► verify
▲ │
└──────────── conditional edge: rejected? retry once ◄─────┘
Nodes exchange typed state (dicts validated by JSON Schema), not prose."""
import asyncio
import json
from dataclasses import dataclass, field
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from demo_util import PROJECT
@dataclass
class State:
files: list[str] = field(default_factory=list)
findings: dict[str, list] = field(default_factory=dict)
report: str = ""
verdict: dict = field(default_factory=dict)
cost: float = 0.0
trace: list[str] = field(default_factory=list)
async def node(state: State, name: str, prompt: str, schema: dict | None = None, **opts):
"""Run one graph node as its own fresh-context agent and return its typed output."""
options = ClaudeAgentOptions(
cwd=PROJECT, setting_sources=[], max_turns=opts.pop("max_turns", 6),
output_format={"type": "json_schema", "schema": schema} if schema else None, **opts,
)
out, text = None, None
async for m in query(prompt=prompt, options=options):
if isinstance(m, ResultMessage):
out, text = m.structured_output, m.result
state.cost += m.total_cost_usd or 0
state.trace.append(f"{name}: {m.subtype}, {m.num_turns} turns, ${m.total_cost_usd:.4f}")
return out if schema else text
FILES_SCHEMA = {"type": "object", "properties": {"files": {"type": "array", "items": {"type": "string"}}},
"required": ["files"]}
FINDINGS_SCHEMA = {"type": "object", "properties": {"findings": {"type": "array", "items": {
"type": "object", "properties": {"line": {"type": "integer"}, "issue": {"type": "string"},
"severity": {"type": "string", "enum": ["low", "medium", "high"]}},
"required": ["line", "issue", "severity"]}}}, "required": ["findings"]}
VERDICT_SCHEMA = {"type": "object", "properties": {"approved": {"type": "boolean"}, "reason": {"type": "string"}},
"required": ["approved", "reason"]}
async def plan(state: State):
out = await node(state, "plan", "List every Python source file under src/ (relative paths).",
FILES_SCHEMA, allowed_tools=["Glob"])
state.files = out["files"]
async def fan_out(state: State):
async def worker(path):
out = await node(state, f"worker[{path}]",
f"Review {path} for bugs and security issues. Report findings with line numbers.",
FINDINGS_SCHEMA, allowed_tools=["Read"], effort="low")
return path, out["findings"]
results = await asyncio.gather(*(worker(p) for p in state.files)) # parallel edges
state.findings = dict(results)
async def synthesize(state: State):
state.report = await node(
state, "synthesize",
"Write a short prioritized review (max 8 bullets) from these findings. Do not read files.\n"
+ json.dumps(state.findings, indent=2), tools=[])
async def verify(state: State):
state.verdict = await node(
state, "verify",
"You are an independent reviewer with fresh context. Approve this report only if every finding "
"cites a line number and the highest-severity items come first. Report:\n" + state.report,
VERDICT_SCHEMA, tools=[])
async def main():
state = State()
await plan(state)
print("plan ->", state.files)
await fan_out(state)
print("fan_out ->", {k: len(v) for k, v in state.findings.items()})
for attempt in range(2): # bounded retry edge
await synthesize(state)
await verify(state)
print(f"verify (attempt {attempt + 1}) ->", state.verdict)
if state.verdict["approved"]:
break
print("\nREPORT:\n" + state.report)
print("\nTRACE:"); print("\n".join(" " + t for t in state.trace))
print(f"TOTAL COST: ${state.cost:.4f}")
asyncio.run(main())
101 lines · Back to the folder