Try this first
Give Rover read_file and ask this:
“Read notes.txt and summary.txt, and tell me if they disagree.”
Print what came back:
reply = llm.send(messages, TOOLS)
print([c.name for c in reply.tool_calls])
You will see two tool calls in one reply.
What you just did
One assistant turn can contain many tool calls. The model asked for both files at once,
because neither depends on the other. This is normal, it is good, and it is a large part of
why agents feel fast.
Your loop from Lesson 1.7 already handles it — it walks every call and collects every result
before sending anything back. That was not an accident. It was the point.
The mistake
Here is code that looks equivalent and is not:
# WRONG — one message per result
for call in reply.tool_calls:
output = run_tool(call.name, call.arguments)
messages.append({
"role": "tool_results",
"results": [{"id": call.id, "content": output}],
})
Run it. It works. No error, no warning, correct answer.
Then keep using it, and over the next few turns the model quietly stops making parallel calls.
It reads its own conversation history, sees that its parallel calls came back split into
separate turns, and adapts to a pattern of one call at a time. Your agent gets slower and
costs more, for no visible reason.
The rule
All results from one assistant turn go back in one user message.
# RIGHT
results = []
for call in reply.tool_calls:
output = run_tool(call.name, call.arguments)
results.append({"id": call.id, "content": output})
messages.append({"role": "tool_results", "results": results})
One message. A list of results. Every call in the turn gets exactly one matching result, keyed
by its id.
If a tool fails, you still return a result for it, with is_error: True. Dropping it is not
an option — a missing result for a call id is an error on the next request, on every provider.
Why this lesson exists
There is no error message. There is no warning. The behaviour degrades over turns, and
nothing points at the cause.
This is the shape of most real agent bugs: not a crash, but a silent behaviour change several
turns after the mistake. Recognising that shape is worth more than memorising this particular
rule.
Every tool call in a turn gets a result, and all of them travel in one message. Splitting
them teaches the model to stop calling in parallel.
Turning it off on purpose
Occasionally you want one call at a time — a tool with side effects where order matters, or a
step that needs approval:
Most providers have a switch for this — Anthropic spells it
tool_choice={"type": "auto", "disable_parallel_tool_use": True}, others differ. Check yours.
Use it deliberately, not as a way to avoid writing the loop correctly.
Try this before the next lesson
Deliberately write the wrong version. Run a session of six or seven turns, asking for two
files each time.
Watch the number of tool_use blocks per turn. It usually drops to one within a few turns.
Now you have seen a silent behaviour change with your own eyes, and you will recognise the
next one faster.