"""Pattern 5: a supervised swarm. Peers investigate competing hypotheses, share a blackboard, and a judge decides."""
import asyncio
import json
import shutil
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from demo_util import PRISTINE, PROJECT
# work on a pristine copy so the bugs are present regardless of earlier demos
WORK = Path(tempfile.mkdtemp(prefix="debate-")) / "project"
shutil.copytree(PRISTINE, WORK)
async def run(prompt, schema=None, **opts):
options = ClaudeAgentOptions(cwd=str(WORK), setting_sources=[], max_turns=opts.pop("max_turns", 6),
output_format={"type": "json_schema", "schema": schema} if schema else None, **opts)
async for m in query(prompt=prompt, options=options):
if isinstance(m, ResultMessage):
return (m.structured_output if schema else m.result), (m.total_cost_usd or 0)
@dataclass
class Blackboard:
hypotheses: dict[str, str]
evidence: dict[str, list[str]] = field(default_factory=dict)
cost: float = 0.0
EVIDENCE_SCHEMA = {"type": "object",
"properties": {"for": {"type": "array", "items": {"type": "string"}},
"against": {"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}},
"required": ["for", "against"]}
VERDICT_SCHEMA = {"type": "object", "properties": {"winner": {"type": "string"}, "reason": {"type": "string"},
"confidence": {"type": "number"}}, "required": ["winner", "reason", "confidence"]}
async def investigate(bb: Blackboard, name: str, theory: str):
others = {n: t for n, t in bb.hypotheses.items() if n != name}
out, c = await run(
f"You are investigator {name}. The failing test is tests/test_utils.py::test_user_name_none_returns_empty "
f"(it calls get_user_name(None) and expects ''). Your theory: {theory}.\nOther open theories: {json.dumps(others)}\n"
f"Evidence already on the blackboard:\n{json.dumps(bb.evidence, indent=2)}\n"
"Read the code and the test. Return evidence FOR your theory and evidence AGAINST each other theory "
"(cite file:line). Be honest: if your theory is wrong, say so in 'for' with an empty list.",
EVIDENCE_SCHEMA, allowed_tools=["Read", "Grep", "Glob"], effort="low")
bb.cost += c
bb.evidence.setdefault(name, []).extend(out["for"])
for other, reasons in out["against"].items():
bb.evidence.setdefault(f"against_{other}", []).extend(reasons)
async def debate(bb: Blackboard, rounds: int = 2):
for r in range(rounds): # (1) bounded rounds
await asyncio.gather(*(investigate(bb, n, t) for n, t in bb.hypotheses.items())) # (2) peers in parallel, shared blackboard
print(f"after round {r + 1}: evidence counts = { {k: len(v) for k, v in bb.evidence.items()} }")
verdict, c = await run("Judge which hypothesis best explains the failing test given this evidence. "
"Prefer the theory with concrete file:line support and no rebuttals.\n" + json.dumps(bb.evidence, indent=2),
VERDICT_SCHEMA, tools=[]) # (3) independent judge, no tools
bb.cost += c
return verdict
async def main():
bb = Blackboard(hypotheses={
"A": "get_user_name crashes because it subscripts user['name'] without checking for None",
"B": "the test fails because of an import error: src.utils cannot be imported",
"C": "calculate_average's division by zero is what makes the whole test module fail",
})
verdict = await debate(bb)
print("\nBLACKBOARD:")
for k, v in bb.evidence.items():
print(f" {k}:")
for item in v[:3]:
print(f" - {item[:140]}")
print(f"\nVERDICT: {verdict}")
print(f"TOTAL COST: ${bb.cost:.4f}")
asyncio.run(main())
84 lines · Back to the folder