The agentic loop
- Send requestmessages + tools + system prompt
- Claude respondstext, tool requests, or both
- Check
stop_reasontool_useorend_turn? - Run the toolsappend
tool_resultblocks
tool_use → go round again · end_turn → return the final answer
stop_reason of end_turn.The loop, precisely
An agentic loop is four steps repeated: send the conversation to Claude, inspect the response, execute any tools it asked for, and append the results so the next request can reason about them. The model — not your code — decides what to call next.
- Send the messages, system prompt and tool definitions.
- Read
stop_reasonon the response. - If it is
tool_use: run each requested tool, and append the assistant turn and a user turn containing the matchingtool_resultblocks. - If it is
end_turn: the model has finished. Return its final text.
messages = [{"role": "user", "content": task}]
while True:
response = client.messages.create(
model=MODEL, max_tokens=4096, tools=TOOLS, messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
break # end_turn — or a stop you must handle
results = []
for block in response.content:
if block.type == "tool_use":
output = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id, # ties the result to the request
"content": output,
})
messages.append({"role": "user", "content": results})Two details carry most of the marks. The whole assistant turn goes back into history, not just its text. And every tool_use block gets a tool_result with the same tool_use_id — one response can request several tools at once, and each needs its own answer.
One turn, message by message
tool_use block, id toolu_01get_order("A-1042"){ status: "shipped" }tool_resultend_turnModel-driven versus pre-configured
The guide draws a line between a loop where Claude chooses the next tool from context, and a decision tree or fixed sequence that your code walks through. Neither is wrong. A fixed sequence is predictable and cheap; model-driven selection handles inputs you did not anticipate. The exam asks you to pick the one the scenario calls for — which usually turns on how varied the inputs are.
| Stop signal | Reliable? | Why |
|---|---|---|
stop_reason == "end_turn" | Yes | The API's own statement that the model finished |
stop_reason == "tool_use" | Yes — keep going | The model is waiting on tool results |
| Assistant text contains “done” | No | Prose is not a protocol; phrasing varies |
| Response has any text content | No | Claude often explains and calls a tool in one turn |
| Iteration cap reached | Only as a backstop | Stops good runs and hides the real bug |
Every stop reason, and what to do about it
“Not tool_use” does not mean “success”. The API documents several other values, and a production loop handles each one on purpose:
stop_reason | What happened | Handle it by |
|---|---|---|
end_turn | The model finished | Returning the answer |
tool_use | The model wants a tool run | Running it and looping |
max_tokens | Output hit your max_tokens limit mid-answer | Raising the limit or continuing — never treating the partial text as complete |
stop_sequence | One of your custom stop sequences fired | Checking which one, then acting on it |
pause_turn | A server-side tool loop hit its iteration limit | Sending the assistant content back so it can continue |
refusal | The model declined | Reading the details; retrying or escalating — not looping |
model_context_window_exceeded | The response filled the context window | Treating it as truncated and trimming context |
Reading the stop reason
stop_reason?tool_useRun tools, loop againend_turnDone — return answermax_tokens·model_context_window_exceededTruncated — don't trust itrefusalStop and escalate
The same loop, inside the Agent SDK
You rarely write this loop by hand in production. The Claude Agent SDK runs it for you: it sends the request, executes built-in and custom tools, appends results, and repeats until Claude replies with no tool calls. Each round trip is one turn. What you get back at the end is a ResultMessage whose subtype tells you how the loop ended.
Who runs the loop
Messages API — you write the loop
- You check
stop_reasonafter every response - You execute each tool and build the
tool_resultblocks - You decide the iteration cap and what hitting it means
Agent SDK — the SDK runs it
- The loop ends when Claude replies with no tool calls
- Built-in tools (
Read,Bash,Grep…) run automatically, subject to permissions max_turns/maxTurnsandmax_budget_usd/maxBudgetUsdare backstops
ResultMessage.subtype | Meaning | Is result present? |
|---|---|---|
success | Claude finished the task | Yes |
error_max_turns | Hit max_turns before finishing | No |
error_max_budget_usd | Hit the spend cap before finishing | No |
error_during_execution | Something interrupted the loop | No |
Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Ending the loop when the reply sounds finished | End on stop_reason == "end_turn". |
| Treating any text content as completion | A turn can contain text and tool_use together; check stop_reason. |
| An iteration cap as the main exit | Keep a cap as a backstop and treat hitting it as a failure. |
| Appending only the tool output, not the assistant turn | Append the full assistant content, then a user turn of tool_result blocks. |
You should now be able to
- Write loop control that continues on
tool_useand stops onend_turn. - Append tool results between iterations so the model reasons over new information.
- Recognise and remove prose-parsing, text-presence and cap-as-exit stopping logic.
- Choose between model-driven tool selection and a fixed sequence for a given workflow.