The Agentic Loop and stop_reason
How the model-tool-client loop actually terminates, and why stop_reason is the only signal you can trust to control it.
Every agentic system built on the Claude API runs the same underlying loop: the model receives the conversation so far, decides whether to call a tool, the client executes that tool and appends the result, and the whole thing repeats. There is no magic scheduler underneath this. The loop is just a while-loop in your own code, and the entire question of "when do we stop" comes down to one field on the API response: stop_reason.
On each turn, Claude returns a response with a stop_reason that tells you why generation stopped. The four values you need to know cold for the exam are:
end_turn— the model believes it has finished responding to the user. No further tool calls are pending. This is your signal to exit the loop and hand control back to the human.tool_use— the model wants to call one or more tools. You must execute them and feed the results back as a new user turn containingtool_resultblocks before continuing.max_tokens— generation was cut off because it hit the token limit, mid-thought. This is not a natural stopping point; the response may be truncated JSON or a half-written tool call.stop_sequence— the model hit a custom stop sequence you configured. Also not a natural end of task.
The correct control-flow pattern is simple: after every API call, branch on stop_reason. If it's tool_use, execute the requested tools and loop again. If it's end_turn, exit. If it's max_tokens or stop_sequence, treat it as an error condition — log it, possibly retry with adjusted parameters, but do not silently continue as if the task finished normally.
Why not just read the text?
A tempting shortcut is to look at the assistant's text output and guess whether it "sounds done" — scanning for phrases like "Task complete" or "I have finished." This fails for several concrete reasons. Models phrase completion inconsistently across turns. A response can contain both prose and a tool call in the same turn, so text presence tells you nothing about whether more tool calls are coming. And relying on natural-language parsing couples your control flow to model phrasing, which changes across model versions and is not a contract Anthropic guarantees. stop_reason is a structured, documented field specifically designed to be machine-readable; text content is not.
stop_reason directly — checking for a content block type is a weaker, indirect proxy and the exam will present a scenario where it diverges (e.g., a malformed or truncated response) from the stop_reason-based check.Iteration caps are a safety net, not a control mechanism
Production agent loops should include a maximum iteration count. But its job is to prevent runaway cost and infinite loops when something goes wrong upstream — not to be the primary way you decide the task is done. If your loop's actual termination logic is "stop after N iterations regardless of stop_reason," you have built a timer, not an agent. The correct design uses stop_reason == "end_turn" as the primary exit condition and an iteration cap purely as a defensive ceiling that should, in a correctly functioning system, almost never be the thing that actually ends the loop.
Scenario: An agent is wired to call a search tool repeatedly. On iteration 40 of a 50-iteration cap, the model returns a response with no tool_use blocks and stop_reason of end_turn, but the assistant text happens to include the word "next" because it's summarizing next steps for the user. A text-scanning implementation misreads this as "more work to do" and keeps looping past a completed answer. The correct implementation exits immediately because stop_reason is end_turn, regardless of the words used in the response.
Try it
Using the Claude API directly (not the SDK's agent loop helpers), write a bare tool-use loop for a single tool of your choice. Log the stop_reason on every turn. Then deliberately set max_tokens low enough that a tool call gets truncated, and observe what stop_reason comes back and how your loop should react to it versus how it reacts if you naively assume any non-empty response means success.