The Agentic Loop: What Actually Ends It
Domain 1.1, the heaviest objective on the exam: the agentic loop driven by stop_reason, the full set of stop reasons, the Agent SDK's autonomous loop and its ResultMessage subtypes, and the three loop-termination anti-patterns the blueprint wants you to recognize on sight.
Domain 1 — Agentic Architecture & Orchestration — is 27% of the exam, the single heaviest slice, and its first task statement is the foundation for everything else: design and implement agentic loops for autonomous task execution. Most people can describe an agent loosely (“it uses tools in a loop”) and then miss the exam items, because the items test the mechanics — what drives an iteration, and specifically what ends one. This post covers that precisely, at both levels the exam cares about: the raw Messages API loop you drive yourself, and the Agent SDK loop that drives itself.
Code here is written against anthropic 0.120.0 and claude-agent-sdk 0.2.128. The API shapes — stop_reason values, message block fields, ResultMessage subtypes — are introspection-verified and noted as such; the examples that require a live Claude call are marked ⚠️ and will be executed in this series’ dedicated key-based pass.
The loop, at the raw API level
An agent is a language model in a loop with tools. At the lowest level — the anthropic Messages API — you run that loop, and it turns on one field: stop_reason. Each turn, you send the conversation to Claude with a set of tools; Claude replies; you inspect why it stopped; if it stopped to call a tool, you run the tool, append the result, and go again.
import anthropic
client = anthropic.Anthropic()
messages = [{"role": "user", "content": "What's the status of order A17?"}]
while True:
resp = client.messages.create(
model="claude-haiku-4-5",
max_tokens=1024,
tools=tools, # your tool schemas
messages=messages,
)
if resp.stop_reason != "tool_use":
break # Claude is done talking — this is the answer
messages.append({"role": "assistant", "content": resp.content})
results = []
for block in resp.content:
if block.type == "tool_use": # block.id, block.name, block.input
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})
print(resp.content[0].text) # final answer, stop_reason == "end_turn"
That is the entire mechanism, and it’s worth reading twice because every part is exam-relevant. Verified against the SDK: the assistant reply carries tool_use content blocks each with an id, name, and input; you run the named tool with that input; and you return the output as a tool_result block whose tool_use_id matches the request’s id. The loop continues while stop_reason == "tool_use" and terminates on end_turn. (⚠️ The loop’s runtime behavior — Claude actually choosing tools and finishing — is documented from the API contract, pending the live execution pass; the message shapes above are introspection-verified.)
The critical insight, and a common exam target: you loop on stop_reason, not on anything you read out of the text. The model tells you why it stopped as a structured field. You never parse its prose to decide whether it’s finished.
The full set of stop reasons
The exam’s task statement frames it as “tool_use vs end_turn,” but a well-designed agent handles all of them, and items may probe the edges. Verified — anthropic 0.120.0’s Message.stop_reason is exactly:
Literal["end_turn", "tool_use", "max_tokens", "stop_sequence",
"pause_turn", "refusal", "model_context_window_exceeded"]
What each means for your loop:
tool_use: Claude wants a tool run. Execute, append results, continue. (the loop continues)end_turn: Claude finished its turn with a normal reply. (the loop ends)max_tokens: the response hit themax_tokenscap mid-generation. The output is truncated, not complete; you handle it (raise the cap, or continue), you don’t treat it as an answer.stop_sequence: a stop sequence you configured was emitted. Expected, if you set one.pause_turn: a long-running turn was paused (used with certain server-side tools); you send the response back to continue it.refusal: Claude declined to continue for safety reasons; treat as terminal, surface appropriately.model_context_window_exceeded: the conversation outgrew the context window. This is a reliability signal, and it’s exactly what Domain 5’s context management exists to prevent — an agent that ignores it just fails.
Knowing this set is knowing that “end the loop when it’s not tool_use” is almost right but incomplete: max_tokens and refusal aren’t “answers,” and the naive if resp.stop_reason != "tool_use": break above would treat a truncated max_tokens response as a finished one. A production loop checks for end_turn explicitly and handles the rest.
The loop, at the Agent SDK level
Writing that loop by hand is instructive and, most of the time, not what you’d ship. The Claude Agent SDK runs the loop for you. You call query() and consume a stream of messages; the SDK handles the tool-use iterations internally and signals completion with a final ResultMessage.
from claude_agent_sdk import query, ClaudeAgentOptions
async for message in query(
prompt="What's the status of order A17?",
options=ClaudeAgentOptions(allowed_tools=["mcp__shop__lookup_order"]),
):
... # SystemMessage, AssistantMessage, UserMessage, StreamEvent, then a ResultMessage
Here you do not inspect stop_reason yourself — the SDK abstracts the whole tool_use→run→continue cycle. Instead, termination is reported by the final ResultMessage, whose subtype tells you how the run ended. Verified — the SDK’s ResultMessage carries subtype, num_turns, total_cost_usd, usage, result, is_error, and more, and the subtypes are:
success— the agent completed the task.error_max_turns— it hit themax_turnslimit.error_max_budget_usd— it hit themax_budget_usdcap.error_during_execution— an error occurred mid-run.error_max_structured_output_retries— structured-output validation failed too many times.
This maps cleanly onto the raw picture: subtype == "success" corresponds to the loop reaching end_turn, while the error_* subtypes are the guardrails (turn limit, budget) that stop a runaway. (⚠️ live query() execution pending the key pass; the ResultMessage fields and subtypes are introspection-verified.)
When to use which is a real exam-adjacent judgment. Reach for query() / the SDK when you want the standard agent loop with built-in tools, MCP, and guardrails — which is most of the time. Drop to the raw Messages API loop when you need to own the control flow: custom termination logic, injecting steps between tool calls, or a non-standard iteration pattern. Both are legitimate; the exam wants you to know that the SDK’s loop is the raw loop, managed, so you can reason about it either way.
Model-driven vs. pre-configured
One more distinction the blueprint draws explicitly, because it separates an agent from a script. In an agentic loop, Claude decides which tool to call next based on the current context — it reasons over the conversation and the tool results so far and chooses. That is different from a pre-configured decision tree or a fixed tool sequence, where your code decides the order and the model just fills in arguments.
The agentic approach is what lets a support agent handle an ambiguous request: it might call get_customer, see the account is delinquent, and decide to call escalate_to_human rather than process_refund — a path you didn’t hard-code. The exam favors model-driven control for open-ended tasks and reserves fixed sequences for genuinely deterministic flows. If an item describes an agent that “always runs tools A then B then C regardless of results,” that’s a pre-configured pipeline masquerading as an agent, and usually the wrong design for the ambiguous scenarios the exam poses.
The three anti-patterns to recognize on sight
This is the highest-value part of the post, because the blueprint names these as anti-patterns, which means the exam will offer them as tempting wrong answers. Each is a way of ending the loop that isn’t stop_reason, and each is wrong for the same underlying reason: it substitutes a fragile heuristic for the structured signal the API already gives you.
-
Parsing the model’s natural language to decide the loop is done. Watching for the model to say “I’m finished” or “here is your answer” and terminating on that. Wrong because the text is not a contract — the model might say “let me check that” and then stop, or produce an answer without any such phrase. The
stop_reasonfield is the contract; the prose is not. This is the anti-pattern most likely to appear as a plausible distractor. -
Using an arbitrary iteration cap as the primary stopping mechanism. Capping at “10 iterations” and calling that your loop’s control. Wrong because a cap is a safety backstop, not a completion condition — a task that legitimately needs 12 tool calls fails, and a task that finished in 2 wastes nothing but taught you nothing about why it stopped. Caps like
max_turnsandmax_budget_usd(the SDK’serror_max_turns/error_max_budget_usdsubtypes) exist precisely as backstops against a runaway; leaning on them as the main exit is designing for the failure case instead of the success case. -
Checking for assistant text content as a completion indicator. Treating “the assistant produced some text” as “the agent is done.” Wrong because a
tool_useturn can also contain text — Claude often narrates (“Let me look that up…”) in the same message it requests a tool. Terminate on text-present and you’ll stop the loop mid-task, before the tool it just asked for ever runs.
All three share a fix, and it’s the whole lesson of the post: terminate on the structured stop_reason (or the SDK’s ResultMessage.subtype), never on a heuristic read of the output. When an exam item asks how to end a loop, the answer that inspects stop_reason/subtype is right, and the three above are the traps.
Final thoughts
The agentic loop is send-inspect-execute-repeat, driven by stop_reason at the raw API level and by the SDK’s managed loop with its ResultMessage subtypes one level up. The exam’s Domain 1.1 rewards knowing the full set of stop reasons (not just tool_use/end_turn), understanding model-driven control versus a pre-configured pipeline, and — above all — recognizing the three termination anti-patterns as wrong on sight. Get the loop’s control flow right and the rest of Domain 1 is elaboration on it: more agents, and the plumbing between them.
Next: multi-agent orchestration — coordinator–subagent systems, what a subagent can and can’t see, and how to decompose a task without leaving gaps.
Comments