~/$GRAPHS//AgentsGraph Agent Orchestration · v1.0
01 · Foundation

Why Multi-Agent?

A single agent hits three walls: the context window (one conversation cannot hold a whole codebase audit), specialization (one system prompt cannot be a security reviewer and a copywriter at once), and parallelism (one loop reads one file at a time). Multi-agent systems split work across many model contexts so each stays small, focused, and fast.

The honest trade: you pay for it in latency, tokens, and compounding errors. Every agent boundary is a lossy channel — the worker never knows what the lead didn't write down.

🧠
Context isolation
Each agent gets a fresh window. Tool spam and file dumps stay in the worker; only conclusions flow up.
🎯
Specialization
A narrow system prompt with three tools beats a kitchen-sink prompt with thirty for its niche.
⚡
Parallelism
Ten reviewers over ten modules finish in one module's wall-clock time.
🐢
Cost: latency
Every hop adds a full model round-trip. Deep chains feel slow even when each step is fast.
💸
Cost: tokens
Context gets re-explained at every boundary. A 5-agent pipeline can burn 10× a single agent's budget.
📉
Cost: error compounding
A 95%-reliable step, chained five times, is a 77%-reliable system. Verification stages exist to buy that back.
The one-line rule
Add an agent when the work exceeds one context window or one skill set — not because the architecture diagram looks better with more boxes.
01 · Foundation

Agents as Graphs

Every multi-agent system is a directed graph: nodes are agents or plain functions, edges carry control flow (who runs next) and data flow (what they receive), and stateis what accumulates along the way. Naming the graph explicitly is what turns "a pile of prompts calling each other" into something you can test, replay, and reason about.

graph-anatomy.txt
// A multi-agent system is a graph, whether you drew one or not. // // ┌──────────────┐ // ┌─────▶│ researcher A │──┐ // │ └──────────────┘ │ // ┌────┴───┐ ┌──────────────┐ │ ┌────────────┐ // │ lead │─▶│ researcher B │──┼──▶│ synthesizer │──▶ answer // └────────┘ └──────────────┘ │ └────────────┘ // │ ┌──────────────┐ │ // └─────▶│ researcher C │──┘ // └──────────────┘ // // Nodes = agents (or plain functions — not everything needs a model) // Edges = control flow (who runs next) + data flow (what they receive) // State = what accumulates as the graph executes

The Execution Loop as a State Machine

Deterministic code should own the edges; the model should own the nodes. Routing decisions the business logic already knows ("after review, verify") belong in code — reserve model judgment for decisions that need it ("is this finding real?").

📥
Ingest
goal enters the graph
🗺️
Plan
decompose into nodes
🤖
Execute
agents run the nodes
⚖️
Verify
judges check outputs
📤
Synthesize
one answer exits
Design move
Draw the graph before writing any prompt. If you cannot draw the edges, the agents will not discover them for you at runtime — they will just loop.
01 · Foundation

Single Agent vs Swarm

The strongest 2026 systems are not maximally multi-agent — they are minimally multi-agent. A single agent with good tools, a tight loop, and room to think outperforms a swarm on any task that fits in one context.

Swarm smell
Agents added for org-chart vibes
Every step waits on every other
Context re-explained at each hop
No single place to debug a run
Errors compound, nobody verifies
Earned multi-agent
Work exceeds one context window
Subtasks are truly independent
Workers return summaries, not logs
Graph is explicit and replayable
Verification is its own stage

Decision Heuristic

Ask three questions. Does it fit? If the whole task fits in one window, stay single. Is it parallel? If subtasks do not depend on each other, fan out. Does it need a second opinion? If being wrong is expensive, add independent verifiers — that is multi-agent too, and it is usually the first upgrade worth paying for.

fits one window → singleindependent parts → fan outhigh stakes → add judgessequential + deep → pipeline
02 · Topologies

Orchestrator–Worker

The workhorse topology. A lead agent decomposes the goal, dispatches narrow subtasks to workers, and synthesizes their results. Workers never talk to each other — all coordination flows through the orchestrator, which keeps the graph a star instead of a hairball.

orchestrator.ts
// Orchestrator–worker: the lead decomposes, workers execute // in parallel, the lead synthesizes. Workers never talk to // each other — all coordination flows through the orchestrator. type Subtask = { id: string; objective: string; context: string }; async function orchestrate(goal: string) { // 1. Decompose — one model call, structured output const plan = await lead.plan(goal); // returns Subtask[] // 2. Fan out — each worker gets a NARROW objective and // only the context it needs, not the whole conversation const results = await Promise.all( plan.map((task: Subtask) => worker.run({ objective: task.objective, context: task.context, maxTurns: 10, // hard stop — workers must terminate }).catch(() => null) // a dead worker is data, not a crash ) ); // 3. Synthesize — the lead sees summaries, not transcripts return lead.synthesize(goal, results.filter(Boolean)); }

What Makes It Work

Narrow objectives
"Find auth bugs in src/auth" beats "help with the audit". Vague subtasks produce duplicate, drifting work.
Summaries flow up
Workers return conclusions. The lead's context holds N summaries, never N transcripts.
Hard stop conditions
Max turns, max tokens, explicit done-criteria. An unbounded worker is a budget leak.
Failure mode
The lead becomes the bottleneck: if it forwards whole transcripts or re-plans after every worker, you have rebuilt a single agent with extra latency. The lead plans, dispatches, and synthesizes — nothing else.
02 · Topologies

Pipelines & DAGs

When work has stages — find, then verify, then fix — the naive shape is a sequence of barriers: run all finders, wait, run all verifiers, wait. The better shape is a per-item pipeline: each item flows through its own chain independently.

pipeline-vs-barrier.ts
// Two ways to run items through stages A → B → C: // ❌ Stage barriers: every item waits for the slowest peer const a = await Promise.all(items.map(stageA)); const b = await Promise.all(a.map(stageB)); // barrier const c = await Promise.all(b.map(stageC)); // barrier // ✓ Per-item pipelining: item 1 can be in stage C while // item 9 is still in stage A. Wall-clock = slowest single // chain, not the sum of slowest-per-stage. const results = await Promise.all( items.map(async (item) => { const a = await stageA(item); const b = await stageB(a); return stageC(b); }) ); // Only add a barrier when stage N genuinely needs ALL of // stage N-1 — e.g. dedup across findings before an expensive // verification pass.

When a Barrier Is Actually Correct

A barrier is justified only when stage N needs cross-item contextfrom all of stage N−1: deduping findings before expensive verification, early-exiting when the total count is zero, or a synthesis prompt that references "the other results". "The stages are conceptually separate" is not a reason — that is what the pipeline already models.

Smell test
If the code between two Promise.allcalls is a pure flatten/map/filter with no cross-item dependency, the barrier is costing you the fast items' idle time for nothing.
02 · Topologies

Swarms & Handoffs

In a handoff swarm there is no boss: peer agents pass the conversation to whichever specialist fits, like a call being transferred. The elegant trick — popularized by OpenAI's Swarm and Agents SDK — is that a handoff is just a tool call: the runtime swaps the system prompt and tool set, and the history rides along.

handoff.ts
// Swarm handoff: transferring control is just a tool call. // The current agent decides it is the wrong specialist and // hands the conversation to a peer. const tools = [ { name: 'transfer_to_billing', description: 'Hand off when the user asks about invoices, ' + 'refunds, or payment methods.', execute: () => ({ handoff: 'billing-agent' }), }, { name: 'transfer_to_support', description: 'Hand off for bugs, outages, or how-to questions.', execute: () => ({ handoff: 'support-agent' }), }, ]; // The runtime loop watches for a handoff marker: let agent = triageAgent; while (true) { const result = await agent.step(conversation); if (result.handoff) { agent = registry[result.handoff]; // swap the system prompt continue; // + tools, keep the history } if (result.done) return result.answer; }

Where Swarms Shine (and Sink)

Shine
Triage → specialist routing
Conversational, user-facing flows
Cheap to add a new specialist
Sink
Ping-pong loops between two peers
No global view of progress
Batch/parallel work — use a DAG
Guardrail
Always cap total handoffs per conversation. Two agents that each think the other is responsible will transfer forever, politely.
02 · Topologies

Judge Panels

Generation is cheap; being wrong is expensive. A judge panel spends tokens to buy reliability: produce N independent attempts from different angles, score them with independent judges, and synthesize from the winner. For findings (bugs, claims), run the panel adversarially — each judge tries to refute, and only survivors ship.

judge-panel.ts
// Judge panel: N independent attempts, M independent judges. // Diversity beats redundancy — give each judge a different lens. const attempts = await Promise.all([ solver.run(task, { angle: 'simplest thing that works' }), solver.run(task, { angle: 'performance first' }), solver.run(task, { angle: 'edge cases first' }), ]); const scored = await Promise.all( attempts.map(async (attempt) => { const verdicts = await Promise.all( ['correctness', 'security', 'maintainability'].map((lens) => judge.run({ attempt, lens }) // returns { pass, notes } ) ); const passes = verdicts.filter((v) => v.pass).length; return { attempt, passes, verdicts }; }) ); // Majority vote — and keep the losers' best notes for synthesis const winner = scored.sort((x, y) => y.passes - x.passes)[0];

Panel Design Rules

Independence
Judges must not see each other's verdicts. Shared context turns three opinions into one opinion, three times.
Diverse lenses
Correctness, security, does-it-reproduce. Diversity catches failure modes redundancy cannot.
Default to reject
Adversarial judges should refute when uncertain. A plausible-but-wrong finding that survives is the worst outcome.
Budget rule
Scale the panel to the stakes: quick check → one judge; production audit → 3–5 adversarial votes with majority rule.
03 · State & Comms

Shared State vs Messages

Agents must share what they learn. The two archetypes: a blackboard — one store every node reads and writes — and message passing— each node only sees what is addressed to it. Most real systems mix both: a blackboard for the run's working memory, messages for control flow.

state-models.ts
// Blackboard (shared state): every node reads/writes one store. type Blackboard = { goal: string; findings: Finding[]; // append-only is safest status: 'exploring' | 'verifying' | 'done'; }; // Message passing: nodes only see what is addressed to them. type Message = { from: string; to: string; payload: unknown; }; // Rule of thumb: // - blackboard → small graphs, tight loops, easy debugging // - messages → large graphs, isolation, replayable audit log // The failure mode of shared state is write races and stale // reads; the failure mode of messages is lost context.
Blackboard
Trivial to inspect and debug
Great for tight revise loops
Append-only sidesteps races
Messages
Strong isolation between agents
Replayable audit log for free
Scales past one process/machine
Practical default
Start with an append-only blackboard keyed by node name. Reach for messages when you need isolation, replay, or distribution — not before.
03 · State & Comms

Fan-out / Fan-in

The map-reduce of agents: discover a work list cheaply, map workers over it in parallel, reduce the results. The two details that separate a clean fan-out from an expensive mess are deduplication before the expensive stage and tolerating holes in the results.

fanout.ts
// Fan-out / fan-in over an unknown-size work list. // Discover first (cheap), then map (parallel), then reduce. const files = await scout.list('src/**/*.ts'); // discover const findings = ( await Promise.all( files.map((f) => reviewer.run(f).catch(() => null)) ) ) .filter(Boolean) .flat(); // Dedup BEFORE the expensive pass — verifying the same finding // five times because five workers hit it is pure waste. const unique = dedupeBy(findings, (x) => x.file + ':' + x.line); const confirmed = await Promise.all( unique.map((x) => verifier.run(x)) );
Sizing
Cap concurrency (a queue, not a thundering herd), and scale worker count to the work list — 100 items do not need 100 simultaneous contexts, they need ~10 slots and a queue.
03 · State & Comms

Failure & Retry

In a 20-worker fan-out, something willfail: a rate limit, a malformed tool call, a worker that talks itself in circles. The system's quality is defined by what happens next. The core rules: a failed worker returns null, not an exception that kills the run; retries are idempotent (same task id, duplicate completions ignored); and reduced coverage is reported, never silent.

retry.ts
// Failure containment: a null result is data, not a crash. async function runWorker(task: Subtask, attempt = 1): Promise<Result | null> { try { return await worker.run(task); } catch (err) { if (attempt >= 3) { log('worker gave up', { task: task.id, err }); return null; // the reducer must tolerate holes } // Idempotent re-dispatch: same task id, so a duplicate // completion from a zombie worker is safely ignored. await sleep(2 ** attempt * 1000); return runWorker(task, attempt + 1); } } // The reducer treats missing results as reduced coverage — // and SAYS so. Silent truncation reads as "covered everything". const results = all.filter(Boolean); if (results.length < all.length) { report.caveat(all.length - results.length + ' subtasks failed'); }
Bulkhead
One worker's crash must not sink its siblings. Catch at the dispatch boundary.
Backoff + jitter
Synchronized retries from 20 workers recreate the rate-limit storm that caused them.
Honest reducer
"17/20 modules reviewed; 3 failed" is a result. "Reviewed the codebase" after 3 silent failures is a lie.
04 · Execution

Framework Patterns

The frameworks converge on the same shape with different names. LangGraph makes the state machine explicit — typed state, nodes, conditional edges. OpenAI Agents SDK models edges as handoffs. Claude Code models workers as subagents with isolated contexts. Durable engines (Temporal, Inngest) make the graph survive crashes.

state-graph.ts
// Typed state-machine graph (LangGraph-style pseudocode). // Explicit nodes, explicit edges, conditional routing. type GraphState = { question: string; draft?: string; critique?: string; revisions: number; }; const graph = new StateGraph<GraphState>() .addNode('draft', async (s) => ({ draft: await writer.run(s.question), })) .addNode('critique', async (s) => ({ critique: await critic.run(s.draft), })) .addNode('revise', async (s) => ({ draft: await writer.revise(s.draft, s.critique), revisions: s.revisions + 1, })) .addEdge('draft', 'critique') .addConditionalEdge('critique', (s) => s.critique === 'LGTM' || s.revisions >= 3 ? 'END' : 'revise' ) .addEdge('revise', 'critique'); // Same shape everywhere: OpenAI Agents SDK models this as // handoffs, Claude Code as subagents, Temporal as workflows. // The graph is the durable part; the agents are replaceable.
Portable insight
The graph is the durable, testable part of your system; the agents inside the nodes are swappable. Invest in the graph — model upgrades are free wins if the edges do not change.
LangGraph · state machinesAgents SDK · handoffsClaude Code · subagentsTemporal · durable graphs
04 · Execution

When NOT to Multi-Agent

The most common multi-agent architecture mistake is having one. These are the signs you should collapse the graph back into a single agent with better tools:

Collapse the graph if…
The whole task fits in one context window · subtasks are sequential and share almost all context · your "agents" are really function calls with prompts · latency matters more than coverage · you cannot explain what each agent uniquely knows · the verification stage is the only agent doing real work.

The Cost Math Nobody Runs

A 5-hop chain at 95% per-hop reliability lands at 77%. A 3× token multiplier on a task that a single agent solved at 90% is not an upgrade — it is an expensive way to add failure modes. Multi-agent earns its costs on breadth (parallel coverage of independent work) and verification (independent judges), and almost nowhere else.

Upgrade path
Single agent → single agent + judge → orchestrator with 2–3 workers → full graph. Stop at the first rung that meets the quality bar.
04 · Execution

Orchestration Checklist

Run this before shipping any multi-agent system to production.

Graph Design
The graph is drawn: every node, edge, and stop condition is explicitCritical
Deterministic code owns routing; models own judgment calls onlyDesign
Each agent has a one-sentence answer to "what do you uniquely know?"Design
Runtime Safety
Every worker has max-turns, max-tokens, and timeout capsCritical
Handoffs/loops are capped; ping-pong between peers is impossibleCritical
Worker failure returns null; the reducer tolerates and reports holesResilience
Retries are idempotent and backoff includes jitterResilience
Quality & Cost
High-stakes outputs pass an independent (ideally adversarial) judgeQuality
Workers return summaries, not transcripts, to the orchestratorCost
Token spend per run is measured and has a budget alarmCost
You tried the single-agent version first and it measurably lostCritical
Multi-Agent Reference
topologies · state · handoffs
experiments.tyler.lab