The agent loop, by hand
Every helper in the previous lesson is wrapping this. Learn it once at wire level and every product on that list becomes obvious, including the ones that have not shipped yet.
The five beats
One. Send messages plus a tools array. Nothing special, this is just a request.
Two. Read stop_reason. end_turn means the model is finished talking and you are done. tool_use means the model wants something run. Those two drive the loop; the full documented set is end_turn, max_tokens, stop_sequence, tool_use, pause_turn, refusal and model_context_window_exceeded. max_tokens is the one that will genuinely embarrass you in production, and pause_turn is the one that belongs in this loop: if you use server tools, their internal loop can hit its iteration cap, and the correct response is to append the assistant turn and send again rather than to treat it as an ending.
Read stop_reason the way you would read the discriminant on a tagged union, because that is what it is. You are not inspecting the text to guess intent; the response tells you which branch you are in, and the whole loop is a switch on that one field.
Three. The assistant's content is a list of blocks. Some are text. Some are tool_use, each carrying an id, a name, and an input object matching your schema. Execute each one.
Four. Build the next message and send the results back.
Five. Append both turns to your message list and go again.
The bit everyone gets wrong the first time
Tool results go back as a user message, containing a tool_result block per call.
Two rules that follow from the shape and are worth saying explicitly. Every tool_use block needs a matching tool_result, so if the model requested three tools in parallel you send three results in one user message, not three messages. And the ids have to match exactly, because that is the only thing stitching a result to its call.
Failure is a content block, not an exception
Your tool will throw. That is not an error condition for the loop, it is an input to it.
is_error on the tool_result block lets the model see what went wrong and decide what to do: try a different tool, ask you a question, or tell the user it could not find out. Throwing out of your loop instead removes every one of those options and turns a recoverable hiccup into a dead process.
The thing to actually internalise: an error you hand back is a turn the model gets to use. An exception you raise is a conversation you ended.
Termination, or the infinite bill
Nothing in the protocol stops your loop. A model that keeps asking for tools keeps getting them until something external intervenes, and the something is usually your invoice.
So every hand-rolled loop needs a max-iteration counter, and a decision about what happens when you hit it. Silently returning the last text block is the wrong choice: hitting the ceiling is a real outcome and your caller needs to know it happened. The Tool Runner exists precisely so you do not have to keep getting this right by hand.
Try it yourself
Which role carries the tool result
If you have written against other model APIs, your muscle memory is about to lose you a mark.
The loop, from memory
Five beats. Say them in order without looking, then check the ids.
Walk the tool loop from the first request to termination, naming the stop_reason values and the id that stitches a result to its call.
Trace the next request
The model just returned the turn below. Your loop reads it and builds the next message.
{
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "Let me check that." },
{ "type": "tool_use", "id": "toolu_01A", "name": "get_weather", "input": { "city": "Totnes" } }
]
}
Three calls, one turn
Parallel tool use is on by default, so this arrives in a single assistant turn.
{
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{ "type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": {} },
{ "type": "tool_use", "id": "toolu_02", "name": "get_tides", "input": {} },
{ "type": "tool_use", "id": "toolu_03", "name": "get_moonrise", "input": {} }
]
}
When your tool throws
Your weather tool 500s. The exam wants to know what a competent implementation does next.
Hand-simulate three turns
No network, no keys, no account. A scratch file and your memory. This is the one that makes the wire format stick.
Tick every step to confirm you did it.