Multi-agent is one of the most over-applied patterns. The one durable reason to run a task across several language-model agents instead of one is context isolation: keeping what each agent reads out of every other agent’s window. Not speed, not specialization.
In this lesson, we’ll work out why isolation is worth paying for, what it costs (roughly 4–15× the tokens), how to structure the work, and the ways the pattern makes a system worse. By the end you’ll be able to size the token multiplier for a fan-out, decide when a task is genuinely worth splitting, and defend the choice against a single agent in an interview.
The shape of the pattern
One task string goes in; one answer comes out. In between, exactly three kinds of model call happen. w is the number of workers, chosen at run time by the first call.
flowchart TD
T["Task (one string)"] --> D["1 decompose call<br/>splits the task into w non-overlapping briefs"]
D --> W["w worker runs<br/>each reads a lot, returns a short report"]
W --> S["1 synthesis call<br/>merges the reports into the answer"]
S --> A["Answer (one string)"]
Everything below is either an argument about when this shape is worth it, or detail on how each arrow is built.
Vocabulary
A few terms recur throughout.
- Token: the unit a model reads and writes, roughly three-quarters of an English word. Providers bill per million tokens; every count here is in tokens.
- Agent: a language model called in a loop: you hand it a conversation and a set of tools, and it either answers or asks to run a tool, whose output is appended before the next call.
- Fixed prefix: the system prompt plus tool schemas, resent unchanged on every call. It runs about 6,000 tokens and is where every cost figure below starts.
- Context window: the hard cap on how many tokens an agent can see on one call.
- Orchestrator (or lead), the one agent that sees the whole task, splits it, and merges what comes back.
- Worker (or subagent), an agent started with a blank conversation and one narrow brief.
- Context isolation: the property that makes multi-agent worth doing: each worker’s conversation is a separate object no other agent can see, so what a worker reads never lands in anyone else’s window.
Why isolation is the point
The same task done two ways shows what isolation buys: once by a single agent, once by an orchestrator with two workers.
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/>reads 50k, then discarded"]
O --> W2["Worker B<br/>reads 50k, then 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
The single agent puts everything into one window; every page it fetches stays there, because a conversation only grows. By step 30 it holds 50k tokens of tool output.
The orchestrator splits that reading. Each worker reads its 50k, returns a 1k summary, and its conversation is thrown away the moment it returns, so those 50k tokens never enter any other agent’s window. The orchestrator ends up at roughly 8k tokens: the 6,000-token prefix plus two 1k summaries.
The number that matters is the ratio of what gets read to what gets kept. In the picture it is 50 to 1.
A task, end to end
Take a concrete question: which of our three payment providers has the strictest rate limits, and what should we do about it?
Decompose. The orchestrator turns the question into three subtasks whose scopes do not overlap. Each brief names what not to read:
subtask A "Find Stripe's rate limits and retry guidance.
stripe.com/docs only. Do NOT read Adyen or Braintree."
subtask B "Find Adyen's rate limits and retry guidance.
docs.adyen.com only. Do NOT read Stripe or Braintree."
subtask C "Find Braintree's rate limits and retry guidance.
developer.paypal.com/braintree only. Do NOT read the others."
Work. Each worker starts with an empty conversation holding only its brief. Worker B reads six documentation pages (about 9,900 tokens each), peaking near 60,000 tokens in its window. It does not know workers A and C exist.
Report. Worker B returns one ~800-token report that carries provenance, source and retrieval date, so the synthesizer can break ties later, and states plainly what it could not find:
[B] Adyen: 100 requests/second per merchant account, burst to 200 for 10s.
429 responses carry Retry-After. Bulk endpoints 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.
Discard and synthesize. Worker B’s 60,000-token conversation is freed on return. The orchestrator holds three ~800-token reports, 2,400 tokens standing in for 180,000 read, and merges them. That 75× compression (180,000 ÷ 2,400) is the mechanism the whole pattern rests on.
Put the two designs side by side over the same eighteen pages:
SINGLE AGENT 6,000 prefix + 18 pages × 9,900 ≈ 184,000 tokens resident,
resent on every remaining turn
ORCHESTRATOR 6,000 prefix + 3 × 800 reports ≈ 8,400 tokens resident;
the 180,000 tokens the workers read never entered this window
The orchestrator reasons over ~8,400 tokens that stand in for ~184,000. That is the trade.
Isolation is a capability argument, not a speed one
Three things compound inside a single long-running agent. Each is a separate reason to split the work. All three are developed in the LLM internals chapter, but the summary here is enough to follow the argument.
1. Quadratic cost. The model API is stateless, every turn resends the whole conversation so far. With a fixed prefix P and a tokens added per turn, the input billed over n turns is about n·P + a·n²/2. The squared term dominates: a page that lands at turn 5 is paid for again on every later turn. For a 40-turn agent with P = 6,000 and a = 1,200, that is roughly 1.18M input tokens billed, about 80% of it from accumulation, not the prefix.
2. Positional decay. Retrieval accuracy is U-shaped in position: a model finds facts reliably near the start and end of its window and much less reliably in the middle, with measured drops of 20–40 points in that middle band. By turn 30 the instructions given at turn 1 have been buried in exactly that band by stacked tool output, so the scope line stops being read and the agent wanders off task.
3. A hard ceiling. Eventually the material does not fit, and what gets dropped is evidence the agent was going to cite.
A subagent burns a large window and returns a small report, so the orchestrator pays for the report, not the exploration. Six workers reading 60k each explore 360k tokens; six 800-token summaries occupy under 5k in the orchestrator.
Be precise about what this claim rests on. On a 200k-token window, 360k of source material does not fit at all, the argument is literal. On a 1M-token window it does fit, and mechanisms 1 and 2 still bite: the same tokens are billed quadratically in one conversation, and they are read worse, because everything past the first stretch 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. Two short contexts are read better than one long one.
The cost: 4–15× the tokens
Isolation is not free. Fanning out reads more material, so it bills more tokens, typically 4 to 15 times what a single agent on the same question would.
That range is a ratio, so name the denominator. B is what a single agent takes, running one context until it decides it has read enough, not one agent doing all the workers’ reading. The numerator is w·W + O: w workers each doing a full run W, plus the orchestrator O (its prefix paid twice, to decompose and to synthesize, plus the reports it reads).
Both B and W are volumes of material one context ends up holding. Build them from one assumed unit: one search-and-read step brings back about 5,000 tokens.
low end single agent: 8 steps → B = 40,000
4 workers × 8 steps → W = 40,000
high end single agent: 5 steps → B = 25,000
6 workers × 12 steps → W = 60,000
The low end is the fair comparison: the single agent reads exactly as much as one worker, so the multiplier lands near the worker count. The high end is what people actually build, more workers, each going deeper, against a single agent that stopped sooner. Both sides of the ratio move, which is where the width of the range comes from.
single agent B | workers w · W | orchestrator O | total | multiplier | |
|---|---|---|---|---|---|
| low end | 40,000 | 4 × 40,000 = 160,000 | 2 × (6,000 + 4×800) = 18,400 | 178,400 | ~4.5× |
| high end | 25,000 | 6 × 60,000 = 360,000 | 2 × (6,000 + 6×800) = 21,600 | 381,600 | ~15.3× |
Two caveats. These are volumes read, not invoices: charging the quadratic resend to both sides would widen the band, not shift it, and prompt caching gives both sides a discount on their repeated prefix, so the ratio survives while the absolute totals drop well below these counts. Read 4–15× as a work-volume band.
Where the money goes
O is only 6–10% of the total in both rows. The expense is w × W, the workers’ reading, not the orchestrator. That points straight at the levers:
| Lever | Effect |
|---|---|
Worker count w | Linear — each worker is a fixed marginal cost |
Worker step budget s | Quadratic within each worker |
| Worker model tier | Roughly 2× between the top and mid tier (look up current prices) |
| Summary length | Linear, orchestrator side only |
Because the worker term dominates, downgrading the orchestrator saves almost nothing; running workers on a cheaper model is the real lever.
What the multiplier measures
It is a work-volume multiplier, not an efficiency penalty, and getting that backwards is the common mistake. The low-end 4.5× compares a single agent that would have done 8 searches against 4 workers doing 8 each, 32 searches against 8. It costs more because it read four times as much.
Held the other way, fan-out is cheaper per unit of work. One agent pays a·n²/2 on a single growing context. Split those same n steps across w workers and each pays a·(n/w)²/2, for a total quadratic term of a·n²/(2w): the quadratic divides by the worker count. Splitting one long context into w short ones is exactly what kills quadratic growth.
Both statements are true on a real run. In the multi-agent research case study, a fan-out doing 32 searches costs $2.68, while a single agent pushed to 25 searches costs $5.78, more money for less coverage. The single agent is not worse because it overflows: it peaks at 145.6k tokens inside a 200k window, so it fits. It loses on recall, because its evidence ends up buried in the low-recall middle.
So: fan out to buy more and better reading, not to cut the bill, the bill goes up. If the question only needs a handful of searches, do not fan out; you would pay orchestration overhead for nothing. A single agent with good context offloading, keeping large artifacts on disk and passing pointers instead of contents, wins whenever the subtasks are not genuinely independent.
Topologies
Isolation settles why to split the work; it says nothing about who calls whom or where control sits. A topology is the wiring: which agents exist, which talks to which, and where the decisions are made. Four are worth knowing.
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 derived above: one lead fans out, a separate synthesis call merges. Control is central.
- Handoff / Swarm has no lead. A triage agent decides the problem is a billing one and transfers the whole conversation to a billing agent, which may transfer again. Each agent owns the interaction while it holds it.
- Pipeline is a fixed chain, research, then write, then edit, same stages every time.
- Debate runs a proposer against a critic and hands both positions to a judge.
| Topology | Control | Best for | Watch out |
|---|---|---|---|
| Orchestrator–Worker | Central | Fan-out research, wide code changes | Worker conflict; synthesis quality |
| Handoff | Passed along | Customer service by domain | Ping-pong; context lost in transfer |
| Pipeline | Fixed | Content production, ETL data jobs | It’s a workflow — don’t call it multi-agent |
| Debate | Adversarial | High-stakes judgment | Expensive; converges to bland consensus |
Workflow or multi-agent?
A pipeline is a workflow, not multi-agent, because the stage list is fixed before the run starts. You decided there would be three stages in that order; the same three run for every input. It becomes multi-agent only when the model decides how many workers there are and what each does. A fixed pipeline is cheaper and far easier to debug. You know the call count before you run, and a failure localizes to one stage. Calling it multi-agent forfeits both and buys nothing.
Two guards handoff needs
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 context; handing over the full transcript makes the receiving agent pay 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 is that, with a judge in the loop, proposer and critic converge on the safest defensible position instead of the correct one. It buys calibration, a better sense of how confident to be, more than insight.
Roles and boundaries
Whichever topology you pick, at some point the orchestrator hands a worker a brief. The line saying what not to touch is the most important one in it.
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
- Decompose into a plan of disjoint scopes, no two can touch the same file, page, or record.
- Dispatch each worker on its own brief.
- Collect structured reports: a fixed set of named fields, not free prose, so they compare mechanically.
- Resolve conflicts where reports disagree, then emit the answer.
What goes in a brief
A good brief carries four things:
- Objective: one checkable sentence.
- Scope boundary: explicitly what not to touch.
- Output contract: a named list of fields to fill in, not “report back.”
- Budget: max steps or tokens.
A brief with none of them against one with all four:
Bad: "Research the auth module"
→ three overlapping reports, roughly 60% duplicated work
Good: "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 with an obvious cause: three workers pointed at the same module without exclusions start from the same entry point and read most of the same files, so each report restates the other two.
The boundary must be explicit because workers cannot see each other. Each one, reasoning locally and sensibly, will follow an interesting thread into another worker’s territory, nothing in its context stops it. The scope line is the only mechanism available, which is why “focus on X” is not a boundary but “do not cover Y or Z” is: naming the excluded categories makes the boundary checkable.
Communication
A brief moves information into a worker; results have to move back out. Three channels do that, each with its own cost. The right default is the filesystem.
flowchart TD
subgraph M1["Message passing"]
A1[Agent A] -->|content copied into B's context| B1[Agent B]
end
subgraph M2["Shared state"]
A2[Agent A] --> S[(Shared store)]
B2[Agent B] --> S
end
subgraph M3["Filesystem (default)"]
A3[Agent A] -->|writes file| F[(Workspace)]
B3[Agent B] -->|reads file| F
end
style M3 fill:#2d6a4f,color:#fff
- Message passing copies content from the sender’s output into the receiver’s context. Explicit and auditable, but relayed content is paid for twice: once as the sender’s output (billed at roughly 5× the input rate) and again as the receiver’s input, which the orchestrator then resends on every later turn.
- Shared state: a keyed store the harness owns (a dict, a Redis key, a SQLite row). Cheap, no relay, but exposed to write conflicts.
- Filesystem: the sender writes a file into a shared workspace and the receiver reads it. Large artifacts move without either agent’s context holding the content.
The harness is your own code around the model: it holds the conversations, runs the tool calls, and decides which agent runs next. The model owns nothing; the harness does.
Why the filesystem is the underrated default
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 its full length twice, and the orchestrator re-pays the input side on every subsequent turn. The filesystem sidesteps both.
What a write conflict looks like
Shared state’s cost 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 just loses a finding. Two fixes: give each worker its own key so writes can never collide (findings/A, findings/B, merged at synthesis), or take a lock per key so the read-modify-write is atomic. As a rule, shared state wins for small structured values both agents must see now, and loses for large artifacts only one agent must see later.
A2A (Agent-to-Agent) is the emerging standard for cross-organization agent communication, agent cards for capability discovery, a task lifecycle, structured messages. It stands to agents roughly as the Model Context Protocol stands to tools. It is early enough that the right move is to know it exists instead of building on it.
Building it
Here the pattern is working code, at three levels of detail.
Pseudocode
What matters in the pseudocode below is the order of the calls.
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 workers concurrently. The calls are network-bound, so wall-clock time is the slowest worker, not the sum, a latency win only, changing no cost. find_contradictions before synthesis is the step people skip, and skipping it produces the averaging failure below. resolve and synthesize are two separate model calls, the pair that made O count as two calls in the cost table.
With a graph framework
Frameworks like LangGraph model the system as a graph: each node is a function, and a shared state object threads through them. Two shapes matter.
A supervisor node routes to the next worker. The model produces a small structured value naming the node to run next, and the framework acts on it:
def supervisor(state) -> Command[Literal["researcher", "coder", "__end__"]]:
decision = llm.with_structured_output(Route).invoke(state["messages"])
return Command(goto=decision.next, update={"messages": [...]})
The routing is data the model produced, not an edge you wired in advance, which is the workflow-versus-multi-agent line again. But keeping the worker’s own chatter out of shared state is a convention you must maintain by hand on every update=.
Compiling the worker as a tool removes that discipline. The worker’s internal messages cannot reach the parent’s state, because the two states are different objects and the function returns a string:
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.invoke starts the worker on a fresh message list containing only the topic; taking the last message discards every intermediate one. The isolation is now structural, not conventional: the same discipline the SDK version below achieves with a local variable.
With the 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. The three things to read for: the Subtask schema (a brief, as data), the messages list inside worker() (that local variable is the isolation), and the two MAX_ constants (the only two lines that bound the loop).
import concurrent.futures as cf
import anthropic
from pydantic import BaseModel
client = anthropic.Anthropic()
MAX_STEPS = 12 # ceiling on any worker's step budget
MAX_WORKERS = 5 # 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: model asks, 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, ...
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})")
WORKER_TOOLS and execute_all(...) are supplied by the surrounding harness: the tools a worker may call (typically a strict subset of the orchestrator’s), and the function that runs a response’s tool calls and returns the results. Five details in the code are worth naming:
- Workers run on a cheaper model. The lead needs judgment; workers mostly need execution. That is the model-tier lever applied to the dominant cost term. On current list prices the top-to-mid gap is nearer 1.7× ($5 vs $3 per million in, $25 vs $15 out) than a round 2×, so look up the two prices instead of quoting a rule of thumb.
- Each worker’s history is local and freed on return. The
messageslist lives insideworker(), so its ~60k tokens cease to exist when the function returns its summary. That is the isolation, in one line of Python. - A truncated worker says so.
max_tokensis not an error. You get a normal response whose last sentence stops mid-word. Without the explicit branch, that truncated report falls through the!= "tool_use"test and the synthesizer treats a cut-off exploration as a complete one. (Pairing an invisible hard cap with a budget the model can see is the two-mechanism pattern in the reliability chapter.) - The synthesizer surfaces conflicts instead of smoothing them, the job of the third sentence in its system prompt.
- Every budget is clamped in the harness.
Subtask.max_stepsis a field the model fills in, andmax_steps=10_000_000validates fine; the number of subtasks is likewise whatever the model returned.ThreadPoolExecutor(max_workers=…)caps concurrency, not head-count, so without the[:MAX_WORKERS]slice a 40-subtask plan runs all forty, five at a time. A number in a prompt is a request; a number in arange()is a rule.
When multi-agent makes it worse
Even an orchestrator built exactly as above can hurt. Six failures account for most of the damage; the worst is a synthesizer that quietly averages two contradicting reports.
| Symptom | Mechanism | Do this instead |
|---|---|---|
| Workers duplicate work | Scopes overlap; workers can’t see each other | Disjoint scopes in the brief |
| Two workers edit one file | No coordination layer | Git worktrees; one writer per path |
| Synthesis vaguer than any report | Averaging contradictions | Force explicit conflict resolution |
| 10× cost, same quality | No isolation benefit to buy | Single agent + context offloading |
| Handoffs ping-pong | No transfer cap | Cap at 3, then escalate to a human |
| Impossible to debug | No per-agent traces | Shared run_id across every agent |
A git worktree is a second checked-out directory backed by the same repository, so two workers can edit files simultaneously without touching each other’s copies; you merge deliberately at the end. A run_id stamped on every log line lets you reassemble a trace of the whole run from workers that never met, the cheapest fix here and the easiest to forget.
The averaging failure
Two workers report a rate limit, 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 is true of neither source. It is unfalsifiable, and it silently discarded the recency signal, 2026 docs against a 2023 blog post, that would have resolved the conflict. Nobody downstream can tell it happened. Both fixes are structural: require provenance fields in the report schema so the tiebreaker is in the data, and instruct the synthesizer that contradictions are output, not input to reconcile.
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 stays invisible until much later. If workers must write, isolate at the filesystem or version-control level (one git worktree per worker) instead of asking them nicely to stay in their lane.
Conclusion
- The one durable reason to go multi-agent is context isolation: two short contexts are read better, and billed less per token, than one long one. Not speed.
- Isolation defeats three compounding failures of a single long context, quadratic resend cost, U-shaped positional decay, and the hard window ceiling.
- It costs roughly 4–15× the total tokens, dominated by
w × W(the workers’ reading), not the orchestrator. Move the bill with worker count, step budget, and model tier. - Go multi-agent only when subtasks are genuinely independent with non-overlapping scopes and their results compose into a summary. Otherwise a single agent with context offloading, or a fixed workflow, is cheaper and easier to debug.
- The load-bearing implementation details: disjoint scopes with explicit exclusions, structured reports carrying provenance, the filesystem as the default channel, budgets clamped in the harness, and a synthesizer that surfaces contradictions instead of averaging them.
A useful decision ladder, stopping at the first line that matches:
- Can one agent do it with good context management? → do that.
- Does one agent choke because tool output floods its window? → isolation helps.
- Are subtasks genuinely independent, with non-overlapping scopes? → fan out.
- Can results be composed by a synthesizer? → orchestrator–worker.
- Otherwise → a workflow with fixed stages.
Further reading
- Anthropic, Building effective agents, when to use workflows vs. agents, and the orchestrator–worker pattern.
- Anthropic, How we built our multi-agent research system, a production fan-out research system and its cost profile.
- Liu et al., Lost in the Middle: How Language Models Use Long Contexts, the source of the U-shaped positional-decay result.
- LangGraph documentation, supervisor and subgraph-as-tool patterns.
- Model Context Protocol and the A2A protocol, standards for tool and cross-agent communication.