Lesson 1.1
Agentic Loops and Stop Reasons
How a Claude agent actually runs, request by request, and why the stop_reason field decides what your code does next.
An agent built on the Messages API is not a single call. It is a loop: your code sends a request, inspects why the model stopped, acts on that reason, and sends the next request. The model never executes anything itself. Every tool call is a request from Claude that your code chooses to fulfil. The exam cares about this because almost every orchestration question reduces to one thing: what does the loop do when it sees a particular stop_reason?
The shape of the loop
A minimal agentic loop looks like this in Python:
messages = [{"role": "user", "content": task}]
while True:
response = client.messages.create(
model=MODEL, # current model ID from the Anthropic docs
max_tokens=4096,
tools=TOOLS,
messages=messages,
)
# Always append the full content list, not just the text.
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
results = []
for block in response.content:
if block.type == "tool_use":
out = run_tool(block.name, block.input)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": out})
messages.append({"role": "user", "content": results})
continue
break # end_turn, max_tokens, stop_sequence, refusal, pause_turn
Three details matter. First, you append response.content in full so that tool_use blocks (and any thinking blocks) stay in the transcript; the API rejects a tool_result whose tool_use_id has no matching tool_use block. Second, when Claude issues several tool calls in one turn, all of the tool_result blocks go back in a single user message. Splitting them across messages works, but it teaches the model to stop calling tools in parallel. Third, the API is stateless: the messages array is the only memory the agent has between requests.
Stop reasons
The stop_reason field tells you why generation ended. The table is worth memorising.
| stop_reason | Meaning | What the loop should do |
|---|---|---|
end_turn | Claude finished naturally | Return the answer; the loop is done |
tool_use | Claude wants one or more tools run | Execute them, return tool_result blocks, call again |
max_tokens | Output hit the max_tokens ceiling | Treat as truncated; raise the limit or continue |
stop_sequence | A custom stop_sequences string was produced | Read stop_sequence to see which one; usually done |
pause_turn | A server-side tool loop paused | Resend the conversation unchanged to resume |
refusal | A safety classifier declined | Check stop_details; do not retry blindly |
max_tokens deserves care. If the cut-off happens in the middle of a tool_use block, the input JSON is incomplete and must not be executed. The remedy is to increase max_tokens (or stream, which removes most practical ceilings) and retry, not to parse what arrived.
pause_turn only appears when server-side tools such as web search run inside the request. The server runs its own internal loop with an iteration limit, and when that limit is reached it hands control back. The correct response is to append the assistant turn and call again with no new user message; the API recognises the trailing server tool block and continues. Adding a "please continue" message is a common mistake.
refusal arrives with HTTP 200, not an error status. The stop_details object is populated only for this reason and carries a category and explanation. Code that reads content[0].text without checking stop_reason first will crash or mislead on a refusal.
Tool errors are part of the loop
When a tool fails, do not drop the result or raise an exception out of the loop. Return a tool_result with is_error: true and a readable message. Claude will typically acknowledge the failure and either retry differently or explain the problem. The same applies to invalid tool input: validate block.input against your schema and send the validation error back as an error result.
Guarding the loop
A loop with no exit other than end_turn is a liability. Production loops add at least one of:
- a maximum iteration count (the Agent SDK exposes this as
maxTurns); - a cost or token budget tracked from
response.usageacross iterations; - a wall-clock timeout;
- a set of tools whose execution requires human approval before the loop continues.
The SDK tool runners (client.beta.messages.tool_runner in Python, toolRunner in TypeScript) implement the request, execute, append cycle for you and expose per-turn hooks for approval and logging. Note that they do not automatically resume pause_turn; that still needs handling.
Exam focus
tool_usemeans "call me back with results"; the model does not run tools and the loop is not finished.- All
tool_resultblocks for one assistant turn belong in a single user message, each carrying the matchingtool_use_id. max_tokensis a truncation signal, not success; a truncatedtool_useblock must never be executed.pause_turnis resumed by resending the conversation unchanged, without a new user message.refusalis a 200 response withstop_details, not an HTTP error; checkstop_reasonbefore reading content.- Append the full
response.content, never just the extracted text, or later turns will fail validation.
Check yourself
5 questions
1.A developer is implementing a manual agentic loop directly on the Messages API. After each response arrives, which field should the loop inspect to decide whether to execute tools, return the final answer, or handle truncation?
2.A team's agent loop receives a response with stop_reason 'tool_use' containing two tool_use blocks. The code executes the first tool and sends its result in a user message, then executes the second and sends a separate user message. Over time Claude stops requesting tools in parallel. What is the correct fix?
3.An agent response returns stop_reason 'max_tokens' and the last content block is an incomplete tool_use block whose input JSON is cut off. What should the loop do?
4.A research agent uses Anthropic's server-side web search tool. Some responses come back with stop_reason 'pause_turn' and a trailing server_tool_use block. The team's loop appends a user message reading 'Please continue' and re-sends, but results are inconsistent. Which design is correct?
5.Which statement correctly distinguishes a stop_reason of 'refusal' from an API error?
