The Agent Loop
The agent loop is the control flow that turns a language model into an agent: a while loop that calls the model, runs any tool the model asks for, appends the result, and calls the model again — repeating until the model replies with text instead of a tool request.
The loop is a dozen lines of control flow: the model chooses which tool to request and when to answer, while the harness executes those requests, keeps the transcript, and enforces limits such as the turn budget.
Here is the part people get backwards. You do not write the agent — you write the loop, and the loop has no idea what the task is, when it's done, or what to do next. It calls the model, checks one flag on the reply (stop_reason), and either runs a tool and goes around again or stops and returns. Every interesting decision — which tool, which arguments, when to quit — happens inside the model. The harness just gives it hands and keeps the lights on.
This is the control-flow lens on a structure you can also meet two other ways: tool-calling zooms in on one tool call on the wire (a single tool call takes two stateless round trips to the model), and the ReAct loop reads the same run as the model's reasoning pad. Same loop, three angles. The runs on screen are real: the multi-step task replays the recorded claude-opus-5-5 run from the ReAct lesson, and the one-shot answer is a separate recorded reply from the same model — curated, not scripted.
How The Agent Loop Works
The loop body is the same four steps every turn. Call the model with the whole conversation so far (messages). Inspect the reply's stop_reason: if it isn't tool_use, the model is done — return its text. Otherwise dispatch every tool the model requested in that reply — current models often request several at once — append all the results to messages as one message, and loop. That's the entire contract.
Two things make it work, and both are easy to miss. First, messages is the agent's only memory. The model is stateless between calls; everything it "knows" mid-task is the array you keep re-sending — the question, its own past replies, and every tool result you appended. Drop a result and it's gone. Second, the loop never decides to stop. stop_reason: tool_use means "the model wants a tool"; stop_reason: end_turn means "the model is answering." The model emits that flag; your code only reads it. So the loop goes around once for each reply that asks for tools, plus once more for the answer — a number you don't know in advance and don't control.
That last point is why there's a max_turns. If the model never stops requesting tools, the while never exits. So a production harness should wrap the loop in a budget — for turn in range(max_turns), the same knob agent SDKs expose as a maximum turn or iteration count — that force-exits with no answer if the model runs long. The budget is a safety backstop, not part of the logic: on a healthy run it's never reached.
The Agent Loop Step by Step
The default run is the multi-step task with max_turns = 10. The question is two-hop: Which year was the director of Aurora Run born? The model can't answer that from its weights — it has to look up the film, find the director, then look up the director. Watch messages grow on the right and the four stations light on the left.
- Start.
messagesholds one entry: the user's question.turn = 0. Nothing has run. - Turn 0 — call. The harness sends
messagesto the model. The reply comes back withstop_reason: tool_useand a request:search["Aurora Run"]. That assistant message is appended tomessages. - Turn 0 — inspect. The gate reads
stop_reason. It'stool_use, so the loop will run the tool and come back — not exit. - Turn 0 — dispatch. Your code, not the model, runs
search["Aurora Run"]. The tool returns page 1: "Aurora Run is a 1998 novel by Henrik Dahl (born 1951); the 2011 film was directed by Mara Voss." Note the trap — the novelist's birth year,1951, is the right shape but the wrong fact. - Turn 0 — append. That tool result joins
messages,turnticks to 1, and the back-edge fires: the loop goes around. - Turn 1 — call. The model, now seeing the page, replies
stop_reason: tool_useagain:search["Mara Voss"]. It stepped past the1951decoy because the director's birth year is what the question needs. - Turn 1 — inspect → dispatch → append.
tool_useagain, so the tool runs and returns page 2: "Mara Voss (born 1974) is a Norwegian film director." It joinsmessages;turnticks to 2. - Turn 2 — call. With both facts in
messages, the model replies with text this time —1974— andstop_reason: end_turn. - Turn 2 — inspect. The gate reads
end_turn. The model is done. - Exit. The loop returns
"1974"and stops. It ran three model calls for two tool calls — the N+1 rule — and your code never once decided the task was finished. The model'send_turndid.
Now flip to one-shot: ask something the model already knows (the default is "What is the capital of France?"). It replies stop_reason: end_turn · "Paris" on the very first call — no tool, no second turn. The loop runs once and exits. Same loop body; the model's behavior alone changed the length. And flip max_turns to 2 on the multi-step task to watch the budget bite: the loop runs turns 0 and 1, then the for range is exhausted before the third call that would have answered — so it returns None, no answer, even though the answer was one call away. The meter goes red. That's the backstop doing its job: it can't see the answer coming, it just counts.
How Many Model Calls?
The cost of an agent run is almost entirely model calls, so that's the number worth reasoning about. Every reply in these runs requests one tool, so a task that needs N tool calls costs N + 1 model calls: one per tool request, plus a final call where the model answers instead of asking. The multi-step run is two tools → three calls; the one-shot is zero tools → one call. When the model requests several independent tools in one reply, they share a single call, so in general N counts rounds of tool requests, not tools. The two searches here can't share a round: the second query needs the first result. The budget caps it: a run can never exceed max_turns model calls, which is the whole point of having one.
Each call re-sends the entire messages array, so context grows with every turn and later calls are more expensive per call than earlier ones. With a stateless API like the one modeled here, there's no hidden server-side conversation — that's why you carry messages yourself, and why a long agent run gets pricier as it goes, not cheaper. Some APIs can store the conversation for you and chain calls by an ID, but each call still reads the whole growing context.
Edge Cases
- A tool that returns nothing useful. A miss ("No matching page found.") is a normal observation, not an error — it's appended to
messageslike any other result, and a competent model reformulates and tries again. The loop handles a hit and a miss identically. - The model answers on the first call. The one-shot case:
stop_reasonisend_turnimmediately, the loop body runs once, no tool fires. Perfectly valid — not every task needs tools. - The budget runs out. If
max_turnsis reached while the model is still asking for tools, the loop force-exits and returnsNone. The honest move is to surface "no answer within budget," never to fake one. - A malformed tool request. If the model names a tool that doesn't exist or sends arguments you can't parse, your dispatch step has to catch it and append an error result so the model can recover on the next turn — the loop must not crash on the model's mistake.
- A stop reason other than these two. A reply can also stop because it hit the output-token limit (
max_tokens, so a tool request in it may be cut off), because the model declined (refusal), or because a long server-side tool turn paused (pause_turn, resumed by sending the conversation back). This lesson's!= "tool_use"check would return all three as a final answer; a production loop handles each one.
Common Mistakes
- Thinking your code decides when the agent is done. It doesn't. The model's
stop_reasondoes. If you add your own "looks finished to me" heuristic to break the loop early, you've overridden the one judgment that belongs to the model. - Trying to engineer the reasoning with glue code. The recurring beginner move is branching
if/elsearound the loop to steer which tool runs when. Choosing the tool is the model's job, and hard-coding it per question turns a general harness into a fixed script. The harness executes requests and enforces limits; it does not make the model's choices for it. - Forgetting the agent is stateless. There's no memory but
messages. Mutate it incorrectly — drop a tool result, reorder replies, skip appending the model's own message — and the next call sees a broken conversation. - Checking
stop_reasonafter running the tool. The guard runs first: inspect the reply, and only dispatch a tool if the model actually asked for one. Run the tool first and you'll try to execute the model's final text answer as if it were a tool call. - Returning parallel results one at a time. When a reply requests several tools, run them all and send every result back in one message before the next call. Most APIs reject a request that leaves a tool call without its result.
- Shipping without a
max_turns. "The model will always stop eventually" is not a guarantee. Without a budget, one looping model is an infinite bill.
A Note on Simplification
This lesson shows the bare control flow. A production harness adds machinery this leaves out: streaming replies token by token, running independent tool calls concurrently, retries and error handling around each model call, the other stop reasons, and context management as messages grows — clearing old tool results or compacting earlier turns into a summary before the context window fills. You also don't always write this loop yourself: SDK tool runners and agent frameworks run it for you, and hosted agent services run it on the provider's side. The recorded runs also use a tiny four-page search tool standing in for real retrieval. None of that changes the shape you just watched — call, inspect, dispatch, append, repeat — but a real agent is this skeleton wearing a lot more clothes. Treat this as the mental model, not a spec to build a framework from.
Keep exploring
A few lessons that share the same technique or lead into the next idea.
Premium players need paid access. Every Explain doc is free.
Found a mistake or something unclear? Leave feedback.