InterviewPrepKit

Home / Learn / Agents & LLMs

06 — Multi-Agent

This chapter teaches when to run a task across several language-model agents instead of one, and — much more importantly — why.

Multi-agent is the most over-applied pattern in the field, so the chapter spends its length on one argument: the only durable reason to use more than one agent is context isolation, not speed.

After reading it you will be able to:

The shape of the thing

Before any mechanism, fix what goes in and what comes out.

A task goes in as one string of text — “which of our three payment providers has the strictest rate limits, and what should we do about it?” — and one answer comes out. In between, exactly three kinds of model call happen. w below is the number of workers, and it is chosen at run time by the first call:

task (one string)
   -> 1 decompose call   : splits the task into w independent briefs
   -> w worker runs      : each reads a lot, returns a short written report
   -> 1 synthesis call   : merges the reports into the answer
answer (one string)

That is the whole pattern. Everything else in this chapter is either an argument about when this shape is worth it, or a detail about how each arrow is implemented.

The vocabulary, defined once

The rest of the chapter leans on these ten terms, so they are all defined here before anything relies on them.


1. The only good reason: context isolation

What does running more than one agent actually buy you? One thing: each worker’s reading stays out of every other agent’s window.

Not parallelism. Not “specialization.” Isolation.

The two designs, side by side

The diagram below shows the same task done twice — once by a single agent, once by an orchestrator with two workers. Look at the token counts inside the boxes; those are the whole argument.

flowchart TD
    subgraph SINGLE["Single agent"]
        S1[Task] --> SC["One context window<br/>50k tokens of tool output<br/>by step 30"]
        SC --> SR[Result]
    end
    subgraph MULTI["Orchestrator + subagents"]
        M1[Task] --> O[Orchestrator<br/>~8k tokens]
        O --> W1["Worker A<br/>50k, discarded"]
        O --> W2["Worker B<br/>50k, discarded"]
        W1 -->|1k summary| O
        W2 -->|1k summary| O
        O --> MR[Result]
    end

    style SC fill:#9d0208,color:#fff
    style O fill:#2d6a4f,color:#fff

On the left, the single agent puts everything into one context window, which is holding 50k tokens of tool output by step 30. Every page it fetched is still sitting there, because a conversation only ever grows. Step 30 is not an arbitrary marker; it is the same turn 30 that the positional-decay argument below turns on.

On the right, orchestrator + subagents splits that same reading. The two worker boxes read 50k, discarded. Discarded means the conversation object is thrown away the moment the worker returns, so those 50k tokens never enter any other agent’s context.

Each worker sends back a 1k summary. That leaves the box marked Orchestrator ~8k tokens. Itemise that number rather than trusting it: roughly 6,000 tokens of fixed prefix, plus the two 1k summaries.

6,000 + 2 × 1,000 = 8,000

The diagram is drawn with two workers at round sizes. The arithmetic later in this section uses six workers at measured sizes — 60k explored, 800-token summaries. Both describe the same mechanism at different scales. The quantity that matters is the ratio of what gets read to what gets kept: 50 ÷ 1 = 50× in the picture, 60 ÷ 0.8 = 75× in the measured run.

One task, decomposed end to end

The heart of the pattern is a large volume of material entering a worker’s window and a small report leaving it. Watch that happen once, on the payment-provider question from the opening.

Step 1 — the orchestrator makes one decompose call. It sees the question and nothing else, and it emits a plan of three subtasks whose scopes do not overlap:

subtask A   objective: "Find Stripe's published rate limits and retry guidance."
            scope:     "stripe.com/docs only. Do NOT read Adyen or Braintree."
            max_steps: 6
subtask B   objective: "Find Adyen's published rate limits and retry guidance."
            scope:     "docs.adyen.com only. Do NOT read Stripe or Braintree."
            max_steps: 6
subtask C   objective: "Find Braintree's published rate limits and retry guidance."
            scope:     "developer.paypal.com/braintree only. Do NOT read the others."
            max_steps: 6

Notice that each scope line says what not to read, by name. That is what makes the three scopes disjoint rather than merely different. Roles and boundaries is entirely about why that line matters.

Step 2 — three workers start, each with an empty conversation. Worker B’s context at its first model call is its brief and nothing else: about 600 tokens. It does not know that workers A and C exist.

(In this example the brief is the worker’s whole prompt — no separate 6,000-token prefix is being charged to it. Giving workers their own prefix would raise every worker figure by a constant; it would not change the shape of the argument.)

Step 3 — worker B reads six documentation pages over six tool calls. The pages average about 9,900 tokens each:

6 pages × 9,900 tokens =  59,400   fetched text
                 brief =     600
                          -------
        context peak    =  60,000 tokens

Step 4 — worker B returns one 800-token report. The report carries provenance — the source and the date it was retrieved — because the synthesizer will later need to break ties between workers that contradict each other:

[B] Adyen: 100 requests/second per merchant account, burst to 200 for 10s.
    429 responses carry Retry-After. Bulk endpoints are metered separately
    at 20 req/s.  [docs.adyen.com/development/rate-limits, retrieved 2026-07]
    Could not determine: the sandbox limit, which is not documented.

Two things to notice in that report: the bracketed source-plus-date, and the explicit “Could not determine” line. A worker that reports what it failed to find is more useful than one that quietly omits it.

Step 5 — worker B’s 60,000-token conversation is discarded. It was a local variable inside the worker function. When the function returns, that variable goes out of scope and the runtime frees the memory behind it. Nothing else in the system ever holds those tokens.

Step 6 — the orchestrator now holds three reports. Three reports at 800 tokens each summarise everything the three workers collectively read:

reports held    3 ×    800 =   2,400 tokens
material read   3 × 60,000 = 180,000 tokens

compression ratio = 180,000 / 2,400 = 75

The compression ratio is the tokens read divided by the tokens those reports occupy when they arrive. The orchestrator’s own 6,000-token prefix sits underneath the reports and is counted separately in the comparison below, because the single agent pays that same prefix too.

The before-and-after

Now put the two designs side by side. This is the comparison the whole pattern rests on.

The single agent would have read the same eighteen pages in one conversation. Count both sides the same way, fixed prefix included on each, and count the single agent at step 18 — the step where it has finished the same eighteen pages the three workers split six apiece:

SINGLE AGENT, one conversation
  6,000 prefix + 18 pages × 9,900            = 184,200 tokens resident by step 18
  every page is still in the window, and is resent on every remaining turn

ORCHESTRATOR, same task, same eighteen pages
  6,000 prefix + question + plan + 3 reports =   8,400 tokens resident at synthesis
  the 180,000 tokens the workers read were never in this conversation at all

The question and the plan are a few dozen tokens each and are ignored in that second sum, which is why it closes exactly: 6,000 + 3 × 800 = 8,400.

The orchestrator reasons over 8,400 tokens that stand in for 184,200. That ratio is the entire product.

One bookkeeping note, because two different step counts appear in this chapter and they are not in conflict. Step 18 is this worked example, where the workload was fixed at eighteen pages. The diagram’s step 30 is the round-numbered sketch, and 30 is also where the positional-decay argument below bites. A single agent working this same task past step 18 keeps accumulating, so it is deeper in trouble at 30, not shallower.

Why isolation is a capability argument, not a speed one

Three mechanisms compound inside a single long-running agent. Each one is a separate reason to split the work.

All three are developed in full in chapter 00, but you do not need that chapter to follow the argument — each is stated here with every symbol and value spelled out inline. The link is there for depth, not as a prerequisite.

Mechanism 1 — quadratic cost

The model API is stateless: it remembers nothing between calls, so every turn resends the entire conversation so far.

Write P for the fixed prefix and a for the material added per turn. The total input billed over n turns is:

n·P  +  a·n(n−1)/2        ≈   n·P + a·n²/2  for large n

The exact coefficient is n(n−1)/2 rather than n²/2 because turn 1 carries no delta — nothing has accumulated yet — so the deltas actually summed are 1 + 2 + … + (n−1).

That squared term is why tool output is expensive out of proportion to its size. A page that lands in context at turn 5 is paid for again on turns 6 through 40.

Substitute a concrete agent — the standing reference agent of Deriving the numbers, with P = 6,000 tokens of system prompt and tool schemas and a = 1,200 tokens added per turn — and run it for forty turns:

linear    n·P          = 40 × 6,000              =   240,000
quadratic a·n(n−1)/2   = 1,200 × (40×39/2)
                       = 1,200 × 780             =   936,000
                                                   ---------
                            total input billed   = 1,176,000 tokens

Of that 1,176,000, the quadratic term alone is 936,000 — about 80%. The prefix is not what is expensive. The accumulation is.

Mechanism 2 — positional decay

Retrieval accuracy is U-shaped in position: a model finds facts reliably near the start and near the end of its window, and much less reliably in the middle. Measured drops are on the order of 20–40 points in the middle band (Why quality degrades in long contexts).

By turn 30, the instructions a single agent was given at turn 1 have been pushed into exactly that middle band by the tool output stacked on top of them.

Name the failure this produces rather than leaving it abstract: the scope line stops being read, and the agent wanders into work it was told not to do. That is the same failure Roles and boundaries later describes for workers, arriving here by a different route.

Mechanism 3 — a hard ceiling

Eventually the material simply does not fit. Then something has to be dropped from the window, and what gets dropped is evidence the agent was going to cite.

Combining the three

A subagent burns a large window and returns a small report, so the orchestrator pays for the report rather than for the exploration:

6 workers × 60k explored  = 360k tokens read
6 summaries × 800         =   4.8k tokens the orchestrator holds
                            ~75× compression

A single agent cannot read 360k tokens of source material and still have room to reason about it. Be precise about which part of that is a window-size claim and which part is not, because an interviewer will push exactly here.

On a 200k-token window the material does not fit at all, so the claim is literal.

On a 1M-token window it does fit — and mechanisms 1 and 2 still bite. The same 360k tokens are billed quadratically in one conversation, and they are read worse, because everything past the first stretch of the window sits in the low-recall middle.

So the claim that survives any window size is about quality and cost per token read, not about running out of room. It is still a capability claim rather than a speed one.

Say it this way: “Fan out because two short contexts are read better than one long one — not because you ran out of room.”

The cost, derived

Isolation is not free. The price is a multiplier on total tokens, and this subsection builds that multiplier from one assumed input so you can reconstruct it rather than recall it.

The structure

Start with the shape, for w workers running s steps each:

orchestrator = decompose(1 call) + w·summary_in + synthesis(1 call)
worker_i     = s steps over its own prefix, quadratic in s
total        ≈ w · worker_loop_cost + orchestration overhead

That is a formula with symbols in it, so substitute real values into it. Otherwise the 4–15× below is a number you memorized rather than one you understand.

Name the denominator first

4–15× is a ratio, and a ratio is meaningless without a denominator.

Call the denominator B: what a single agent takes on the same question, running one context until it decides it has read enough.

B is not one agent doing all the workers’ reading. That is the comparison everybody makes and it is wrong, for the reasons worked out in What the 4–15× is actually measuring below.

The numerator is w·W + O:

Counting the decompose call as if it also read the reports is a deliberate over-estimate. It makes O an upper bound, and the conclusion below survives the padding.

Build B and W

B and W are the two cells everyone quotes without building, so build them.

Both are the same kind of quantity — the volume of material one context ends up holding — and both come out of a single assumed unit: one search-and-read step brings back roughly 5,000 tokens, a page or two of documentation.

That unit is the only stipulated input in this section. Everything after it is multiplication.

low end    a single agent stops after   8 steps  ->  B =  8 × 5,000 = 40,000
           each of w=4 workers runs     8 steps  ->  W =  8 × 5,000 = 40,000
high end   a single agent stops after   5 steps  ->  B =  5 × 5,000 = 25,000
           each of w=6 workers runs    12 steps  ->  W = 12 × 5,000 = 60,000

The low end is the fair fight: the single agent reads exactly as much as one worker does, so the multiplier lands near the worker count.

The high end is what people actually build: more workers, each going half again as deep, measured against a single agent that gave up sooner.

That is where the width of the range comes from — both sides of the ratio move at once. The high end is not “the same comparison with bigger numbers.”

The step size is not sacred, only the volume. The worked example above reached the same W = 60,000 a different way, with six documentation pages at ~9,900 tokens each.

One honest caveat about what these volumes are

They are material read, not invoices.

Mechanism 1 says a context that grows to 60,000 tokens over six turns is billed for a good deal more than 60,000, because every turn resends everything before it.

Charging that quadratic resend to both sides would move both endpoints — and by different amounts, since the fan-out side runs deeper contexts. So it would widen the band rather than shift it cleanly.

Reading B and W as volumes is therefore a deliberate choice rather than a shortcut. 4–15× is being quoted here as a statement about how much material the system reads, which is exactly what the next section argues it is. Quote it as a work-volume band and say so.

Two omissions to state before the interviewer does

Prompt caching. Neither side of the ratio applies the ~0.1× discount a repeated prefix earns. Both a single agent and a fan-out resend a large stable prefix on every turn, so both collect that discount, and the ratio of work volumes survives roughly intact. The absolute totals do not: the real invoices on both sides sit well below these token counts.

A second derivation that lands in the same place. Deriving the numbers reaches these same two endpoints by the other route: it reads B and W as billed totals from n·P + a·n²/2, and it calls its four numbers illustrative bracket ends rather than measurements. Those are two derivations that agree at the endpoints, not one derivation printed twice. Cite whichever you are actually using, and do not borrow the other’s definition of B and W while you do it.

The two configurations

Here are the two ends of the usual range with every cell shown. Read the total column as w·W + O, and the multiplier as total ÷ B.

single agent Bworkers w · Worchestrator Ototalmultiplier
low end40,0004 × 40,000 = 160,0002 × (6,000 + 4×800) = 18,400178,4004.5×
high end25,0006 × 60,000 = 360,0002 × (6,000 + 6×800) = 21,600381,60015.3×

The 2 × in the O column is the decompose call and the synthesis call — two calls, each paying the prefix and the reports.

So 4–15× is not folklore. It is:

low end    178,400 / 40,000 =  4.46  ->  4.5×
high end   381,600 / 25,000 = 15.26  -> 15.3×

Every cell there is reconstructible from the one assumed 5,000-token step.

Where the money actually goes

Notice how small O is:

low end    18,400 / 178,400 = 10.3% of the bill
high end   21,600 / 381,600 =  5.7% of the bill

The orchestrator is not the expense; w × W is the expense. That tells you the levers immediately, which is what separates a memorized number from an understood one:

LeverEffect
Worker count wLinear — each worker is a fixed marginal cost
Worker step budget sQuadratic within each worker
Worker model tier~2× between Opus and Sonnet
Summary lengthLinear on the orchestrator side only

Opus and Sonnet in that third row are model names, not roles. They are two tiers of the same model family — Opus the most capable and most expensive, Sonnet the mid tier — priced per million tokens read and written.

The ~2× is a rule-of-thumb spread between adjacent tiers, not a quoted price ratio. The actual gap moves every time a price list changes, so look up the two per-token prices before you lean on it.

The direction is what matters, and it does not move: the model-tier lever applies to the worker term, which is the dominant one. Downgrading the orchestrator saves almost nothing, because O was 5.7–10.3% of the total to begin with.

What the 4–15× is actually measuring

It is a work-volume multiplier, not an efficiency penalty — and getting this backwards is the most common way to misuse the number.

You are comparing a single agent that would have stopped after 8 searches against 4 workers doing 8 searches each — the low-end row of the table above, where B and W are both eight steps. That is 32 searches against 8. Of course it costs more; it read four times as much.

Watch the basis change

The next claim quietly swaps what is being held fixed, so slow down here.

Both statements are true. They answer different questions, and the phrase “per unit of work” is what carries the switch.

Per unit of work, fan-out is actually cheaper. One agent pays a·n²/2 on a single growing context. w workers splitting that same n steps each pay a·(n/w)²/2, so the total quadratic term is:

w × a·(n/w)²/2  =  a·n²/(2w)

The quadratic divides by the worker count, because splitting one long context into w short ones is exactly what kills quadratic growth.

Both are true at once, on a real run

Case study 04 works the arithmetic on a real research task:

Be careful about why that single agent is worse, because the tempting version of the argument is wrong. It does not overflow: it peaks at 145.6k tokens inside a 200k window, so it fits. What it loses is recall. The evidence it has to cite ends up buried in the middle of the context, which is the position models read worst (mechanism 2 again).

Say it this way: “You fan out to buy more reading, not to cut the bill — the bill goes up. Per page read it is cheaper, because splitting one long context into w short ones divides the quadratic term by w. But if the question only needs eight searches, don’t fan out; you’d be paying orchestration overhead for nothing.”

What interviewers probe: “Why not one agent with more tools?” Usually you should. Go multi-agent when subtasks are independent, each needs deep exploration you don’t want in the main thread, and results compose into a summary. Otherwise a single agent with good context offloading — keeping large artifacts on disk and passing pointers instead of contents, covered in chapter 04 — wins on every axis.


2. Topologies

Isolation settles why to split the work. It says nothing about who calls whom, or who is allowed to decide what happens next — and there are only four answers to that question worth knowing, plus a test that tells a genuine multi-agent system apart from a workflow wearing the name.

A topology here just means the shape of the wiring: which agents exist, which one talks to which, and where control sits.

The diagram below shows all four at once. Read each box for the direction of the arrows: that is where control lives.

flowchart TD
    subgraph A["Orchestrator–Worker"]
        O1((Lead)) --> W1[W] & W2[W] & W3[W]
        W1 & W2 & W3 --> S1[Synthesis]
    end
    subgraph B["Handoff / Swarm"]
        H1[Triage] -->|transfer| H2[Billing]
        H2 -->|transfer| H3[Technical]
    end
    subgraph C["Pipeline"]
        P1[Research] --> P2[Write] --> P3[Edit]
    end
    subgraph D["Debate"]
        D1[Proposer] <--> D2[Critic]
        D1 & D2 --> D3[Judge]
    end

Orchestrator–Worker is the shape The only good reason context isolation just derived. One Lead call fans out to workers W, and a separate Synthesis call merges what they return. Control is central: the lead decides everything.

Handoff / Swarm has no lead at all. A Triage agent decides the customer’s problem is a billing one and transfers the conversation to Billing, which may in turn transfer it to Technical. Each agent owns the whole interaction while it holds it. “Swarm” is simply the name for a set of peers that can hand off to each other.

Pipeline is a fixed chain: Research, then Write, then Edit, in that order, every time.

Debate runs a Proposer and a Critic against each other and hands both positions to a Judge.

Here is the same four, with what each is for and what goes wrong:

TopologyControlBest forWatch out
Orchestrator–WorkerCentralFan-out research, wide code changesWorker conflict; synthesis quality — When multi agent makes it worse is the failure list
HandoffPassed alongCustomer service by domainPing-pong; context lost in transfer
PipelineFixedContent production, extract–transform–load (ETL) data jobsIt’s a workflow — don’t call it multi-agent
DebateAdversarialHigh-stakes judgmentExpensive; converges to bland consensus

The Pipeline row states a verdict, so here is the test behind it

A pipeline is a workflow because the stage list is fixed before the run starts. You, not the model, decided there would be exactly three stages in that order, and the same three run for every input.

It becomes multi-agent at the moment the model decides how many workers there are and what each one does. That is exactly the line between a workflow and a model-directed pattern (Orchestratorworker).

The distinction is worth defending rather than waving at, because a fixed pipeline is cheaper and far easier to debug: you know the call count before you run it, and a failure localises to one stage. Calling it multi-agent forfeits both of those and buys nothing.

Handoff needs two guards everyone forgets

Cap the transfers. Two agents that each believe the other should handle it will bounce forever, and each bounce is a full model call.

Decide what transfers with the customer. Handing over just the last message loses everything. Handing over the full transcript means the receiving agent pays for a conversation it did not have. The usual answer is a structured handoff summary: what was established, what was tried, what is still open.

Debate’s characteristic failure

With a judge in the loop, proposer and critic tend to converge on the safest defensible position rather than the correct one. Debate buys you calibration — a better sense of how confident to be — rather than insight.


3. Roles and boundaries

Whichever topology you pick, at some point an orchestrator hands a worker a brief — and the one line in that brief saying what not to touch is the single highest-leverage sentence in a multi-agent system.

The diagram below is one orchestrator–worker run in four labelled steps. Follow the numbers, not the arrows.

flowchart LR
    L[Lead agent] -->|1. decompose| P[Plan: 3 disjoint scopes]
    P -->|2. dispatch with<br/>explicit scope| W[Workers]
    W -->|3. structured reports| L
    L -->|4. resolve conflicts| R([Answer])

    style L fill:#2d6a4f,color:#fff
  1. Decompose — one model call turns the task into a plan of 3 disjoint scopes. Disjoint means no two of them can touch the same file, page, or record.
  2. Dispatch with explicit scope — each worker is started on its own brief.
  3. Structured reports — each worker sends one back. Structured means a fixed set of named fields rather than free prose, so the reports can be compared mechanically.
  4. Resolve conflicts — the lead decides what to believe where two reports disagree, then emits the answer.

What goes in a brief

The brief determines the outcome. A good one carries four things:

  1. Objective — one sentence, checkable.
  2. Scope boundary — explicitly what not to touch.
  3. Output contract — a schema, meaning a named list of fields the worker must fill in, not “report back.”
  4. Budget — max steps or tokens.

Compare a brief that has none of them against one that has all four:

✗ "Research the auth module"
     → three overlapping reports, ~60% duplicated work

✓ "Read only src/auth/session.py. Return
   {findings: [{file, line, issue, severity}]}.
   Do not read or edit other files. Max 10 tool calls."
     → composable, non-overlapping, bounded

The ~60% is a rough figure rather than a measured one, and its source is easy to see. Three workers pointed at the same module without exclusions each start from the same entry point and read most of the same files, so the majority of each report restates the other two. The precise fraction depends on how big the module is; the shape of the failure does not.

Why the boundary must be explicit rather than implied

Workers cannot see each other. Each one, reasoning locally and sensibly, will follow an interesting thread into another worker’s territory — there is nothing in its context to stop it.

The scope line is the only mechanism available. That is also why “focus on X” is not a boundary and “do not cover Y or Z” is: naming the excluded categories is what makes the boundary checkable.

Case study 04 shows the same brief in both forms against a real research task.


4. Communication

A brief moves information into a worker; results have to move back out. There are exactly three channels for that, each with its own price — and the right default is the one that looks least sophisticated: the filesystem.

The diagram shows the three channels as three pairs of agents. The green box is the one to reach for by default.

flowchart TD
    subgraph M1["Message passing"]
        A1[Agent A] -->|explicit message| B1[Agent B]
    end
    subgraph M2["Shared state"]
        A2[Agent A] --> S[(Shared store)]
        B2[Agent B] --> S
    end
    subgraph M3["Filesystem"]
        A3[Agent A] -->|writes file| F[(Workspace)]
        B3[Agent B] -->|reads file| F
    end

    style M3 fill:#2d6a4f,color:#fff

Under message passing, agent A sends an explicit message whose content is copied into agent B’s context.

Under shared state, both agents read and write a shared store that the harness owns. The harness is your own code around the model: the program that holds the conversations, executes the tool calls, and decides which agent runs next. The model never owns anything; the harness does.

Under filesystem, agent A writes a file into a workspace — an ordinary directory both agents can reach — and agent B reads it. The content moves without either agent’s context holding all of it.

ChannelProCon
Message passingExplicit, auditableRelayed content is paid for twice — once in the sender’s output, once in the receiver’s input
Shared stateCheap, no relayWrite conflicts; needs locking
FilesystemLarge artifacts never enter any contextNeeds path discipline

Why the filesystem is the underrated answer

The reason is the double billing in that table.

A worker that writes report_a.md and returns "wrote report_a.md, 3 critical findings" costs about 10 tokens of orchestrator context.

The same content relayed as a message costs the full length twice: output tokens for the worker, which bill at roughly 5× the input rate, plus input tokens for the orchestrator — and the orchestrator pays those input tokens again on every subsequent turn, because of mechanism 1.

What a write conflict actually looks like

“Needs locking” is not an explanation, so here is the concrete failure.

A shared store is just a keyed container the harness owns and both agents can address: a dictionary in the framework’s state object, a Redis key, a row in SQLite.

A write conflict is the ordinary lost-update race:

worker A reads findings = [f1]
worker B reads findings = [f1]              (same starting value)
worker A writes findings = [f1, f2]
worker B writes findings = [f1, f3]         <- A's f2 is gone, silently

Nothing errors. The run simply loses a finding.

Two fixes:

The rule for choosing between shared state and the filesystem: shared state wins for small structured values both agents must see now, and loses for large artifacts only one agent must see later.

A2A, in one paragraph

A2A (Agent-to-Agent) is the emerging standard for cross-organization agent communication: agent cards that let one system discover what another can do, a task lifecycle, and structured messages.

It stands in the same relation to agents that the Model Context Protocol (MCP) — the standard for exposing tools to a model, covered in Mcp model context protocol — stands in to tools.

It is early enough that the right move is to know that one-liner rather than to build on the protocol.


5. Building it

So far the pattern has been argument and arithmetic. Here it is as working code, at three levels of detail:

Tier 1 — Pseudocode

Five lines. Read them for the order of the calls; each one is named again underneath.

plan = lead.decompose(goal)                 # → disjoint, scoped subtasks
reports = parallel([worker.run(t) for t in plan])
conflicts = find_contradictions(reports)
if conflicts: reports = lead.resolve(conflicts, reports)
return lead.synthesize(reports)

parallel([...]) runs the workers concurrently. The calls are network-bound, so wall-clock time is the slowest worker rather than the sum of all of them. The concurrency is a latency win only; it changes no cost in this chapter.

find_contradictions before synthesis is the step people skip, and skipping it produces the specific failure traced in When multi agent makes it worse.

lead.resolve and lead.synthesize are two separate model calls. That pair is exactly the “orchestration overhead” term O from the cost derivation — which is why O was counted as two calls rather than one.

Tier 2 — LangGraph

LangGraph is a framework that models an agent system as a graph: each node is a function, and a shared state object is threaded through them. Two shapes matter here.

Supervisor — a router node picks the next worker

from typing import Literal
from langgraph.types import Command

def supervisor(state) -> Command[Literal["researcher", "coder", "__end__"]]:
    decision = llm.with_structured_output(Route).invoke(state["messages"])
    return Command(goto=decision.next, update={"messages": [...]})

Two lines carry the whole idea.

Route is a small structured-output schema whose single field next holds the name of the node to run. The model is being made to answer “who goes next?” in a form the code can switch on.

Command(goto=..., update=...) is the edge. The routing is data the model produced on this call, not a graph edge you wired in advance — which is precisely the workflow-versus-multi-agent line from Topologies.

The update={"messages": [...]} slot is what the supervisor forwards into shared state: normally the brief for the chosen worker and nothing more.

What matters as much is what does not come back this way. The worker’s own intermediate messages are not returned through this channel, and keeping them out is a convention you must maintain by hand, on every update=. The next shape removes the need for that discipline.

Subgraph-as-tool — the worker is compiled separately

Here the worker’s internal messages cannot reach the parent’s state. The isolation becomes structural rather than a convention you have to maintain:

worker = worker_graph.compile()

@tool
def research(topic: str) -> str:
    """Research a topic in depth. Returns a summary, not a transcript."""
    out = worker.invoke({"messages": [("user", topic)]})
    return out["messages"][-1].content        # only the summary crosses

worker_graph.compile() turns the worker’s graph into a standalone runnable object with a state of its own. Because it is compiled separately, its state is not the parent’s state, and nothing threads between them implicitly.

The @tool decorator registers the plain Python function research as something the parent agent can call by name, exactly like a search or a fetch. From the parent’s point of view, a whole agent is now indistinguishable from any other tool.

worker.invoke({"messages": [("user", topic)]}) starts that worker on a fresh message list containing only the topic. No parent history goes in.

out["messages"][-1].content takes the last message only — the worker’s final answer — and discards every intermediate message behind it. That is the line the comment is pointing at.

Compare that with the supervisor above, where keeping worker chatter out of shared state was a rule you had to remember every time. Here there is no channel through which the worker’s transcript could reach the parent, because the two states are different objects and the function returns a string.

That is what “structural rather than conventional” means, and it is the same discipline the SDK version below achieves with a local variable.

Tier 3 — Anthropic SDK

A worker is not a special object. It is the same agent loop as the orchestrator, called with its own separate message history. There is nothing magic in it.

Read the block below for three things: the Subtask schema (what a brief is, as data), the messages list inside worker() (that local variable is the isolation), and the two MAX_ constants (the only two lines that make the loop bounded).

import concurrent.futures as cf
import anthropic
from pydantic import BaseModel

client = anthropic.Anthropic()

MAX_STEPS = 12      # harness ceiling on any worker's step budget
MAX_WORKERS = 5     # harness ceiling on how many workers a plan may start

class Subtask(BaseModel):
    id: str
    objective: str
    scope: str          # explicit boundary — what NOT to touch
    max_steps: int      # a REQUEST from the model; the harness clamps it below

class Plan(BaseModel):
    subtasks: list[Subtask]

def decompose(goal: str) -> Plan:
    r = client.messages.parse(
        model="claude-opus-5",
        max_tokens=4096,
        system=("Split the goal into 2-5 INDEPENDENT subtasks. Scopes must not "
                "overlap — two subtasks may never touch the same file or record. "
                "If the goal cannot be split cleanly, return a single subtask."),
        messages=[{"role": "user", "content": goal}],
        output_format=Plan,
    )
    return r.parsed_output

def worker(task: Subtask) -> str:
    """Isolated window. Returns a summary only; the history is discarded."""
    messages = [{"role": "user", "content":
                 f"<objective>{task.objective}</objective>\n"
                 f"<scope>{task.scope}</scope>\n"
                 f"Stay strictly inside the scope. End with a <=200 word summary."}]
    for _ in range(min(task.max_steps, MAX_STEPS)):   # clamp: the model asks, the harness decides
        resp = client.messages.create(
            model="claude-sonnet-5",           # workers cheaper than the lead
            max_tokens=4096,
            tools=WORKER_TOOLS,
            messages=messages,
        )
        if resp.stop_reason == "max_tokens":   # cut off mid-report, NOT finished
            return (f"[{task.id}] TRUNCATED: the report hit max_tokens and is "
                    f"incomplete. Do not treat it as a complete exploration.")
        if resp.stop_reason != "tool_use":      # end_turn, refusal, ...
            # the default is load-bearing: a refusal carries no text block
            return next((b.text for b in resp.content if b.type == "text"),
                        f"[{task.id}] no text returned (stop_reason={resp.stop_reason})")
        messages.append({"role": "assistant", "content": resp.content})
        messages.append({"role": "user", "content": execute_all(resp.content)})
    return f"[{task.id}] step budget exhausted; partial work only"

def orchestrate(goal: str) -> str:
    plan = decompose(goal)
    subtasks = plan.subtasks[:MAX_WORKERS]     # clamp: "2-5" was asked for, not enforced
    with cf.ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
        reports = list(pool.map(worker, subtasks))

    joined = "\n\n".join(
        f"<report id='{t.id}' scope='{t.scope}'>\n{r}\n</report>"
        for t, r in zip(subtasks, reports))

    r = client.messages.create(
        model="claude-opus-5",
        max_tokens=8192,
        system=("Synthesize the worker reports. If two reports contradict each "
                "other, say so explicitly and explain which is better supported. "
                "Never average conflicting claims into a vague middle."),
        messages=[{"role": "user", "content": joined}],
    )
    return next((b.text for b in r.content if b.type == "text"),
                f"synthesis returned no text (stop_reason={r.stop_reason})")

Two names are supplied by the surrounding harness rather than defined here:

Five things worth narrating

1. Workers run on a cheaper model. The lead needs judgment; workers mostly need execution. That is the ~2× rule of thumb from the lever table applied to the dominant cost term. On this repo’s own price list the Opus-to-Sonnet gap is nearer 1.7×$5 against $3 per million in, $25 against $15 out — so look the two prices up rather than quoting the round number.

2. Each worker’s history is local and garbage-collected on return. The messages list lives inside worker(), so when the function returns its summary, the 60k tokens behind that summary cease to exist. That is the isolation, in one line of Python.

3. A worker out of budget says so, and so does a worker that was cut off. The step budget exhausted line covers the first. The max_tokens branch covers the second, which is the one that bites.

max_tokens is not an error: you get a normal response whose last sentence stops mid-word. Without that branch the truncated report falls through the != "tool_use" test and the synthesizer treats a truncated exploration as a complete one.

Pairing that invisible hard cap with a budget the model can actually see is the two-mechanism pattern in chapter 07, and Step 6 failure modes 8 min files the same bug under never fake success.

4. The synthesizer is instructed to surface conflicts, not smooth them. That is what the third sentence of its system prompt is for.

5. Every budget is clamped in the harness, not in the brief. MAX_STEPS and MAX_WORKERS are the two lines that make this loop bounded, and both exist because the alternative is a budget set by the thing being budgeted.

Look at where those two numbers would otherwise come from. Subtask.max_steps is a field the model fills in during decompose(), and max_steps=10_000_000 validates against the schema perfectly well. The number of subtasks is likewise whatever the model returned — the system prompt asks for 2–5 and nothing checks.

Without the slice, a 40-subtask plan would run all forty, five at a time, because ThreadPoolExecutor(max_workers=…) caps concurrency, not head-count. Slice the plan and take a min(), and neither can happen.

This is the same reason Roles and boundaries’s fourth brief item is a budget the worker is told rather than one it chooses, and the same reason safety lives in the harness: a number in a prompt is a request, and a number in a range() is a rule.


6. When multi-agent makes it worse

Even an orchestrator built exactly as above can make a system worse. Six failures account for most of the damage in production, and the worst of them is a synthesizer that quietly averages two contradicting reports.

Read the middle column first. Every fix in the right-hand column follows from the mechanism, not from the symptom.

SymptomMechanismDo this instead
Workers duplicate workScopes overlap; workers can’t see each otherDisjoint scopes in the brief
Two workers edit one fileNo coordination layerGit worktrees; one writer per path
Synthesis vaguer than any reportAveraging contradictionsForce explicit conflict resolution
10× cost, same qualityNo isolation benefit to buySingle agent + context offloading
Handoffs ping-pongNo transfer capCap at 3, then escalate to a human
Impossible to debugNo per-agent tracesShared run_id across every agent

Two cells in that table use terms worth expanding on the spot.

A git worktree is a second checked-out directory backed by the same repository. Two workers can edit files at the same time without touching each other’s copies, and you merge the copies deliberately at the end.

The last row is the cheapest to fix and the easiest to forget: give every agent in one run the same run_id, a single identifier stamped on every log line, so a trace of the whole run can be reassembled from workers that never met.

The averaging failure, concretely

This is the one to be able to trace out loud. Two workers report a rate limit, they disagree, and the synthesizer splits the difference:

worker_1 → "Rate limit is 100 req/min [docs/api-v2, updated 2026-06]"
worker_2 → "Rate limit is 1000 req/min [blog/scaling, published 2023]"

synthesis → "The rate limit is in the hundreds of requests per minute."

That output statement is true of neither source. It is unfalsifiable, and it silently discarded the recency signal — 2026 documentation against a 2023 blog post — that would have resolved the conflict. Nobody downstream can tell this happened.

Two fixes, both structural:

Case study 04 traces the same failure on a live research run.

Read fan-out is safe; write fan-out is not

Parallel research is fine: the worst case is duplicated reading.

Parallel editing corrupts state, and the corruption is invisible until much later.

If workers must write, isolate at the filesystem or version-control level — one git worktree per worker, in the sense defined above — rather than by asking them nicely to stay in their lane.


7. The senior answer

All of the above compresses into an answer you can give out loud: a five-step ladder that ends in “no” more often than “yes”, and one number quoted with its source.

When asked “would you use multi-agent here?”, walk the ladder in order and stop at the first line that matches:

  1. Can one agent do it with good context management? → do that.
  2. Does one agent choke because tool output floods its window? → isolation helps.
  3. Are subtasks genuinely independent, with non-overlapping scopes? → fan out.
  4. Can results be composed by a synthesizer? → orchestrator–worker.
  5. Otherwise → a workflow with fixed stages, cheaper and debuggable.

Then quote the number and its source: roughly 4–15× the tokens, dominated by w × worker context, moved by worker count, step budget, and model tier. If pressed, give the two endpoints: 178,400 / 40,000 = 4.5× at the low end and 381,600 / 25,000 = 15.3× at the high end, from the derivation above.

Being specific about where a cost number comes from is what separates “I read a blog post” from “I have operated one.”


Next: 07 — Reliability & Guardrails.