Tutorials · Claude Agent SDK · examples / python / p2_graph_pipeline.pyExample code

Example code · python

p2_graph_pipeline.py

A code-owned graph. Your program is the orchestrator; each node is a query().

Raw fileDownload all examples (zip)See its output →

Used in Part 4 §7.3 · Implementation 1: a code-owned graph over query()

"""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