Module 1: Why Multi-Agent (and When Not To)
The executed comparison
Description
The four previous lessons built each piece separately: what a multi-agent system is (02), how
much coordinating costs, with a formula (03), and the referee between an agent with more tools
and several agents with fewer tools each (04). This lesson brings the three pieces together over
a real case, end to end, with no formulas or estimates: the SAME Reservo task — "quote Focus pro
3h and book it for Ana" — solved first by one agent with the four canonical tools (exactly
agent-fundamentals M5's runner, unchanged) and then by a two-agent system (a supervisor and
a booking_agent), and you'll count, with the complete history printed on screen, how many model
calls and how many messages each path cost.
The result isn't an opinion: it's a number. For this specific task — one a single agent already solves well — you'll see the two-agent system uses more model calls and pays coordination hops the single agent doesn't need, without gaining anything in exchange: the same final answer, with the same two tool calls. This is the module's centerpiece — the one that turns "coordinating costs" from a claim into a measurement.
Connection to the module
This lesson is the executed synthesis of lessons 02 through 04: it uses the formal definition of
a multi-agent system (02), applies the cost vocabulary — calls, hops — from lesson 03's formula
to the exact n=1 case (a single specialist consulted), and confirms lesson 04's referee with a
real case. The runner you'll use is literally agent-fundamentals M5's —
run_agent_parallel + dispatch_parallel — with no modification: this guide's new
orchestration doesn't replace that runner, it wraps it with a delegation mechanism on top.
Analogy: asking someone for help with a task you already know how to do
Pick back up lesson 01's analogy. If you already know how to draft an email, asking a colleague to review it before sending doesn't automatically make it better — it adds the time to explain what you need, the time for them to read it, and the time for them to hand back their version. If the email was already fine, that whole cycle was an extra round trip with no benefit. That's exactly what this lesson measures: a supervisor that delegates a complete task to a single specialist, when that specialist is — tool for tool — exactly what a single agent already had.
Worked example, System A: one agent with the 4 canonical tools
We reuse agent-fundamentals M5's runner with no changes at all: dispatch_parallel dispatches
a turn's tool_use blocks (one or several, with real threads in parallel); run_agent_parallel
is the while that requests, dispatches, feeds, and repeats. We add only two counting functions,
purely for bookkeeping — they don't touch the runner's logic.
import concurrent.futures
import reservo_tools as rt
TOOLS = {
"list_rooms": rt.list_rooms,
"get_quote": rt.get_quote,
"book_room": rt.book_room,
"cancel_booking": rt.cancel_booking,
}
def dispatch_parallel(tool_use_blocks, tools):
"""The one from agent-fundamentals M5, unchanged."""
with concurrent.futures.ThreadPoolExecutor(max_workers=len(tool_use_blocks)) as pool:
futures = [pool.submit(tools[b["name"]], **b["input"]) for b in tool_use_blocks]
results = [f.result() for f in futures]
return [
{"type": "tool_result", "tool_use_id": b["id"], "content": str(r)}
for b, r in zip(tool_use_blocks, results)
]
def run_agent_parallel(question, model_script, tools, max_iterations=10):
"""The one from agent-fundamentals M4/M5, unchanged."""
messages = [{"role": "user", "content": question}]
for step in range(max_iterations):
turn = model_script[step]
messages.append({"role": "assistant", "content": turn["content"]})
if turn["stop_reason"] != "tool_use":
return turn, messages
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations reached ({max_iterations})")
def count_model_calls(history):
"""NEW in this lesson: every 'assistant' message is one model call
consumed from the script (concept) -- requesting a tool, or the final text."""
return sum(1 for m in history if m["role"] == "assistant")
def count_tool_calls(history):
"""NEW in this lesson: counts the real tool_use blocks dispatched."""
total = 0
for m in history:
if isinstance(m["content"], list):
total += sum(1 for b in m["content"] if b["type"] == "tool_use")
return total
TASK = "Quote Focus pro 3h and book it for Ana"
# Script (concept, claude-sonnet-5): quote, then book, then answer.
model_script_single = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Focus pro 3h costs 6000 cents. I booked the room for Ana "
"(confirmation #1)."
)}]},
]
final_single, history_single = run_agent_parallel(TASK, model_script_single, TOOLS)
print("--- complete history (System A) ---")
for i, m in enumerate(history_single):
role, content = m["role"], m["content"]
if isinstance(content, str):
print(f" [{i}] {role:<9} question: {content!r}")
continue
for block in content:
if block["type"] == "tool_use":
print(f" [{i}] {role:<9} tool_use({block['name']}): {block['input']}")
elif block["type"] == "tool_result":
print(f" [{i}] {role:<9} tool_result: {block['content']}")
elif block["type"] == "text":
print(f" [{i}] {role:<9} final text: {block['text']!r}")
calls_single = count_model_calls(history_single)
tools_single = count_tool_calls(history_single)
print()
print("final answer:", final_single["content"][0]["text"])
print("model calls:", calls_single)
print("tool calls: ", tools_single)
print("hops between agents:", 0)
What to expect (over a fresh, disposable Reservo instance):
--- complete history (System A) ---
[0] user question: 'Quote Focus pro 3h and book it for Ana'
[1] assistant tool_use(get_quote): {'room': 'Focus', 'tier': 'pro', 'hours': 3}
[2] user tool_result: {'price_cents': 6000}
[3] assistant tool_use(book_room): {'room': 'Focus', 'tier': 'pro', 'hours': 3, 'member': 'Ana'}
[4] user tool_result: {'booking_id': 1, 'confirmed': True}
[5] assistant final text: 'Focus pro 3h costs 6000 cents. I booked the room for Ana (confirmation #1).'
final answer: Focus pro 3h costs 6000 cents. I booked the room for Ana (confirmation #1).
model calls: 3
tool calls: 2
hops between agents: 0
Three model calls (turns [1], [3], [5] of the history), two tool calls (get_quote
followed by book_room, in order — book_room needs the confirmed price before booking, so
they can't run in parallel), and zero hops, because there's no other agent to consult. The
6000 the final answer cites is grounded in turn [2]'s tool_result — the same grounding
habit from agent-fundamentals M5 L07.
Worked example, System B: supervisor + booking_agent
Now the same task, exactly the same question, solved by a two-agent system: a supervisor
that decides (concept) to delegate the whole task to a booking_agent, and that booking_agent
solving it by running the same runner, over the same script, as the single agent above.
The message passing between the two is real and executed — a minimal AgentMessage, with who
sends, who receives, the task, and the payload.
from dataclasses import dataclass
@dataclass
class AgentMessage:
sender: str
receiver: str
task: str
payload: dict
ROUTE_CALLS = 1 # concept: the supervisor decides who to delegate to
COMPOSE_CALLS = 1 # concept: the supervisor drafts the final answer for the member
# Step 1 (executed): the supervisor packages the task and sends it to booking_agent.
hop_1 = AgentMessage(sender="supervisor", receiver="booking_agent", task=TASK, payload={})
print("hop 1:", hop_1.sender, "->", hop_1.receiver, "| task:", hop_1.task)
# Step 2 (executed): booking_agent solves the delegated task with the SAME
# runner and the SAME script the single agent used in System A.
final_booking, history_booking = run_agent_parallel(hop_1.task, model_script_single, TOOLS)
print()
print("--- booking_agent's internal history ---")
for i, m in enumerate(history_booking):
role, content = m["role"], m["content"]
if isinstance(content, str):
print(f" [{i}] {role:<9} question: {content!r}")
continue
for block in content:
if block["type"] == "tool_use":
print(f" [{i}] {role:<9} tool_use({block['name']}): {block['input']}")
elif block["type"] == "tool_result":
print(f" [{i}] {role:<9} tool_result: {block['content']}")
elif block["type"] == "text":
print(f" [{i}] {role:<9} final text: {block['text']!r}")
booking_calls = count_model_calls(history_booking)
booking_tool_calls = count_tool_calls(history_booking)
# Step 3 (executed): booking_agent returns its result to the supervisor.
hop_2 = AgentMessage(
sender="booking_agent", receiver="supervisor", task="result",
payload={"text": final_booking["content"][0]["text"]},
)
print()
print("hop 2:", hop_2.sender, "->", hop_2.receiver, "| payload:", hop_2.payload["text"])
# Step 4 (concept): the supervisor composes the final answer for the member.
calls_multi = ROUTE_CALLS + booking_calls + COMPOSE_CALLS
hops_multi = 2
print()
print("model calls:", calls_multi,
f"({ROUTE_CALLS} routing + {booking_calls} booking_agent + {COMPOSE_CALLS} synthesis)")
print("tool calls: ", booking_tool_calls)
print("hops between agents:", hops_multi)
What to expect (over ITS OWN fresh, disposable Reservo instance — which is why book_room
assigns booking_id: 1 again, just like in System A):
hop 1: supervisor -> booking_agent | task: Quote Focus pro 3h and book it for Ana
--- booking_agent's internal history ---
[0] user question: 'Quote Focus pro 3h and book it for Ana'
[1] assistant tool_use(get_quote): {'room': 'Focus', 'tier': 'pro', 'hours': 3}
[2] user tool_result: {'price_cents': 6000}
[3] assistant tool_use(book_room): {'room': 'Focus', 'tier': 'pro', 'hours': 3, 'member': 'Ana'}
[4] user tool_result: {'booking_id': 1, 'confirmed': True}
[5] assistant final text: 'Focus pro 3h costs 6000 cents. I booked the room for Ana (confirmation #1).'
hop 2: booking_agent -> supervisor | payload: Focus pro 3h costs 6000 cents. I booked the room for Ana (confirmation #1).
model calls: 5 (1 routing + 3 booking_agent + 1 synthesis)
tool calls: 2
hops between agents: 2
Notice something important: booking_agent's history, line by line, is identical to System
A's — same get_quote, same price_cents: 6000, same book_room, same booking_id: 1.
booking_agent did nothing different or better than the single agent; it did exactly the
same thing, with the same runner and the same script. The only thing that changed is that a
supervisor had to decide to send it the task (hop_1, plus ROUTE_CALLS) and then had to
compose the final answer for the member from what booking_agent returned (hop_2, plus
COMPOSE_CALLS).
The final comparison
print(f"{'':22}{'System A (1 agent)':>22}{'System B (2 agents)':>24}")
print(f"{'model calls':22}{calls_single:>22}{calls_multi:>24}")
print(f"{'tool calls':22}{tools_single:>22}{booking_tool_calls:>24}")
print(f"{'hops between agents':22}{0:>22}{hops_multi:>24}")
extra_calls = calls_multi - calls_single
pct = extra_calls / calls_single * 100
print()
print(f"difference: System B uses {extra_calls} MORE model calls than System A "
f"({pct:.0f}% more), and {hops_multi} coordination hops that System A doesn't "
f"need -- for the SAME task, with the SAME {tools_single} tool calls, and the same "
f"final answer.")
What to expect:
System A (1 agent) System B (2 agents)
model calls 3 5
tool calls 2 2
hops between agents 0 2
difference: System B uses 2 MORE model calls than System A (67% more), and 2 coordination hops
that System A doesn't need -- for the SAME task, with the SAME 2 tool calls, and the same final
answer.
There's the complete measurement. The same 2 tool calls in both systems — get_quote
followed by book_room, computing the same 6000 cents, generating the same booking_id: 1 —
and the same final answer, word for word. The only real difference between the two paths is
the coordination cost: 2 extra model calls (67% more) and 2 hops the single agent didn't
pay, because it never had to consult anyone. For this specific task — one that doesn't split into
two distinct expertises; quoting and booking both live inside booking_agent's registry — the
multi-agent system added no capability the single agent didn't already have. It only added
coordination.
This is the number behind lesson 01's warning: for a task a single agent with the right tools already solves, adding a supervisor and a specialist doesn't solve it better — it solves it more expensively. Lesson 06 shows the opposite case: a task where consulting two distinct specialists IS actually needed, and where the cost measured here is justified.
Common mistakes
-
Running System A and System B in the same process, without resetting state. If you run both systems' code in the same interpreter, without restarting it in between,
book_roomexecutes twice over the same sharedBOOKINGS, and the secondbooking_idcomes out2, not1— a real mismatch with the script's text (which says "confirmation #1"), not a runner bug. Each system in this lesson assumes its own fresh, disposable Reservo instance. -
Thinking booking_agent "did different or better work." The history above disproves it line by line: it's exactly the same path, same runner, same script. The only difference between the two systems is outside the work itself — in the coordination around it.
-
Subtracting only the calls and forgetting the hops. Hops aren't "free" just because they don't count as model calls — they're real messages that have to be built, sent, and processed. A system with many hops, even if it has few extra calls, still has more moving parts that can fail (revisited from lesson 03, the error surface).
-
Generalizing "multi-agent always costs 67% more" from this single case. The 67% is the result of THIS specific task (
n=1specialist, 3 internal calls) — with a bigger task, or with more genuinely needed specialists, the percentage changes. What does generalize is the pattern: coordination adds a fixed cost (routing + synthesis) on top of work a specialist was already doing. -
Concluding the supervisor pattern "never helps" from this example. This example was chosen on purpose because the task did NOT need two specialists — it's the case of "when NOT to orchestrate." Module 2 builds the complete supervisor pattern, for cases where choosing between several genuinely distinct specialists IS actually needed.
Exercises
Exercise 1: Repeat the comparison with Studio (Easy)
Repeat this lesson's System A and System B, but with the task "Quote Studio basic 2h and book it
for Carlos" (Studio basic 2h = 4000 * 2 = 8000 cents, no discount). Confirm the cost
difference (extra calls, hops) is exactly the same as with Focus.
See solution
TASK_STUDIO = "Quote Studio basic 2h and book it for Carlos"
model_script_studio = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "basic", "hours": 2}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "book_room",
"input": {"room": "Studio", "tier": "basic", "hours": 2, "member": "Carlos"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Studio basic 2h costs 8000 cents. I booked the room for Carlos (confirmation #1)."}]},
]
final_a, history_a = run_agent_parallel(TASK_STUDIO, model_script_studio, TOOLS)
print("System A -- calls:", count_model_calls(history_a), "| tools:", count_tool_calls(history_a))
final_b, history_b = run_agent_parallel(TASK_STUDIO, model_script_studio, TOOLS)
calls_b = 1 + count_model_calls(history_b) + 1
print("System B -- calls:", calls_b, "| tools:", count_tool_calls(history_b), "| hops:", 2)
Expected output:
System A -- calls: 3 | tools: 2
System B -- calls: 5 | tools: 2 | hops: 2
Explanation: the numbers are identical to Focus's — 3 vs. 5 calls, 2 vs. 2 tools, 0 vs. 2 hops — because the coordination cost doesn't depend on which room is quoted or how much it costs: it's the SAME task structure (quote → book, two sequential steps inside a single expertise), regardless of the concrete values. That's exactly what makes this lesson's result a pattern, not a coincidence of Focus pro 3h.
Exercise 2: Measure a three-internal-step task (Medium)
Extend System A's script so that, before quoting, booking_agent first calls list_rooms() (to
confirm "Focus" exists, the grounding habit from agent-fundamentals M1 L08). Run System A and
System B with this 4-internal-step script and confirm how the call difference between the two
systems changes.
See solution
model_script_4steps = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_00", "name": "list_rooms", "input": {}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Focus pro 3h costs 6000 cents. I booked the room for Ana (confirmation #1)."}]},
]
final_a, history_a = run_agent_parallel(TASK, model_script_4steps, TOOLS)
calls_a = count_model_calls(history_a)
print("System A -- calls:", calls_a)
final_b, history_b = run_agent_parallel(TASK, model_script_4steps, TOOLS)
calls_b = 1 + count_model_calls(history_b) + 1
extra = calls_b - calls_a
print("System B -- calls:", calls_b, "| extra calls:", extra)
Expected output:
System A -- calls: 4
System B -- calls: 6 | extra calls: 2
Explanation: System A went from 3 to 4 calls (one more for the new list_rooms step), and
System B went from 5 to 6 — also one more — because the new step is added inside
booking_agent, not in the supervisor. The difference between the two systems stays at 2
extra calls, exactly as lesson 03's supervised_cost formula predicted: the supervisor's
fixed cost (1 routing + 1 synthesis) doesn't depend on how many internal steps the specialist
has — it only depends on how many specialists are consulted (n=1 throughout this module).
Exercise 3: What would happen with two real specialists? (Hard)
Without running any code yet, use lesson 03's supervised_cost(n_specialists=2, calls_per_specialist=3) formula to predict how many calls and hops a system would have where the
supervisor DOES need to consult two distinct specialists (for example, booking_agent and
policy_agent) for a compound task. Then design — in prose, without running the full runner yet,
that's lesson 06 — what that compound task would need to have for the cost difference against a
single agent with all 5 tools together (4 from booking + search_docs) to be worth it.
See solution
The prediction with the formula:
def supervised_cost(n_specialists, calls_per_specialist=3):
route_calls = 1
compose_calls = 1
return {
"model_calls": route_calls + calls_per_specialist * n_specialists + compose_calls,
"hops": 2 * n_specialists,
}
print(supervised_cost(2))
{'model_calls': 8, 'hops': 4}
For it to be worth it: the task would need to genuinely require both expertises at the
same time — not just one of them, as in this lesson's case, where booking_agent alone could
already solve everything. A real example: "quote Focus pro 3h and tell me the cancellation
policy" — neither question depends on the other, and neither lives inside the other's domain
(booking numbers vs. policy text). In that case, the cost of 8 calls and 4 hops isn't compared
against the 3 calls of a single agent that ALREADY could solve everything — because a single
agent with 5 tools together (booking + search_docs) could, in theory, also solve the complete
task — it's compared against the benefits lesson 06 measures separately: the possibility of
running the two sub-questions in parallel (not sequentially, since they're independent) and
the isolation of each specialist's context. Lesson 06 does that complete accounting, not just the
cost.
Summary and next step
- We ran, end to end, the SAME Reservo task — "quote Focus pro 3h and book it for Ana" — with a
single agent and with a two-agent system (supervisor +
booking_agent). - The real numbers: System A used 3 model calls, 2 tool calls, 0 hops; System B used 5 model calls (67% more), the same 2 tool calls, 2 hops — to produce exactly the same final answer, word for word.
- System B's
booking_agentdid nothing different or better than the single agent: it ran the same runner (run_agent_parallel, unchanged fromagent-fundamentalsM4/M5) over the same script, and produced an identical history. The extra cost came entirely from the coordination around it — the supervisor's routing and final synthesis — not from the work itself. - This is the central evidence for "when NOT to orchestrate": a task that already lives complete inside a single specialist's expertise doesn't benefit from adding a coordinator on top.
Next lesson: 06 — When multi-agent actually helps. With the cost already measured in real numbers, we look at the other side: a task that genuinely needs two distinct expertises, and why the same coordination cost is justified there.
Additional resources
- Anthropic — Multi-agent research system — Anthropic's report on the real cost (more tokens, more calls) of a multi-agent system versus a single-agent one, the same kind of measurement this lesson runs by hand.
- Anthropic — Building effective agents — The guiding principle behind this lesson: start simple, and add agents only when the measurement justifies it.
- Anthropic — Messages API reference — The exact shape of
tool_use/tool_result/stop_reasonthis lesson's runner respects, unchanged fromagent-fundamentals. - Python —
dataclasses— The module used forAgentMessage, the minimal shape of the package one agent hands another in this guide. - Python 3.14 — What's New — The version every line of this comparison ran on.