C1 · lesson 1 · 10 min read
The agentic loop and stop conditions
How a tool-using agent cycles between the model and your code, and every way that loop is supposed to end.
Հայերեն
Official video by Anthropic on YouTube. Pressing play loads it from youtube-nocookie.com. ·open on YouTube
An agent is, at its core, a model calling tools in a loop. Your code sends the conversation and the tool definitions. The model either answers or asks for one or more tool calls. Your code runs those tools, sends the results back, and calls the model again. Everything else in this course (subagents, handoffs, approvals) is built on this cycle, so it pays to know exactly how it starts, turns and stops.
One turn, step by step
- Call the Messages API with
messagesandtools. - Append the assistant response to
messages, unchanged. - Read
stop_reason. If it’stool_use, run everytool_useblock in the response and reply with onetool_resultper block, matched bytool_use_id. - Repeat until the model stops asking for tools, or until one of your guards fires.
The tool_result message has strict formatting rules. It must come straight after
the assistant message that requested the tools, with nothing in between. In that
user message the tool_result blocks come first, and any text goes after them.
If a tool fails, send the error text as the content with is_error: true. The
model can then retry with different input or explain the failure. Write errors the
way you’d want them on-call: “kubectl timed out after 30s; the API server may be
overloaded” helps far more than “failed”.
Stop reasons you must handle
stop_reason |
What it means | What your loop does |
|---|---|---|
end_turn |
The model finished | Exit and use the text |
tool_use |
It wants tools run | Run them, send results, loop |
pause_turn |
A server-side tool loop paused (not an error) | Send the assistant content back as-is and call again |
max_tokens |
Your max_tokens cut the output |
Raise the limit or continue; never act on a cut-off tool_use |
model_context_window_exceeded |
The context window filled up | Treat as truncated; trim or summarise history |
stop_sequence |
One of your stop_sequences matched |
Check stop_sequence and carry on |
refusal |
The model declined | Read stop_details; don’t retry blindly in a loop |
Only end_turn (and a stop_sequence you planned for) means “done as intended”.
Everything else is either “keep going” (tool_use, pause_turn) or “something cut
the turn short”.
Stop conditions you own
The model decides when it’s finished, but you decide when it must stop. The
API docs say it plainly: tool_use and pause_turn can repeat indefinitely, so
every production loop needs an iteration cap. Common guards:
- Max steps. A hard limit on model calls. When you hit it, fail loudly. Don’t return whatever text happens to be lying around as if it were an answer.
- Budget. Track token usage per call and stop above a spend threshold.
- Wall clock. An incident bot that answers after 20 minutes has missed the point.
- No progress. The same tool called with the same input three times in a row is a loop, not an investigation.
HTTP errors such as 429 or 529 are a separate channel. Handle them with retries and backoff around the API call, not as stop reasons.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
MAX_STEPS = 8
def run_agent(task, tools, run_tool):
messages = [{"role": "user", "content": task}]
for step in range(MAX_STEPS):
resp = client.messages.create(
model="claude-opus-5-5", max_tokens=2048,
tools=tools, messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason == "tool_use":
results = []
for block in resp.content:
if block.type != "tool_use":
continue
try:
out, failed = run_tool(block.name, block.input), False
except Exception as exc: # report it to the model, don't hide it
out, failed = f"{type(exc).__name__}: {exc}", True
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": out, "is_error": failed})
messages.append({"role": "user", "content": results})
elif resp.stop_reason == "pause_turn":
continue
elif resp.stop_reason == "end_turn":
return resp
else:
raise RuntimeError(f"stopped early: {resp.stop_reason}")
raise RuntimeError(f"no final answer after {MAX_STEPS} steps")
Recap
- The loop is: call the model, read
stop_reason, run every requested tool, send matchingtool_resultblocks, repeat. end_turnis the normal exit.tool_useandpause_turnmean “continue”.max_tokens,model_context_window_exceededandrefusalmean the turn was cut short.- Report tool failures with
is_error: trueand a useful message. - Always add your own caps (steps, budget, time), and fail loudly when they fire.
See it in action
1/5 You send a prompt, plus the tool definitions, to the Messages API.
all steps
- You send a prompt, plus the tool definitions, to the Messages API.
- Claude replies with stop_reason tool_use: it wants a tool run. Your code runs the tool; the model never executes anything itself.
- Your code sends the result back as a tool_result block in the next user turn and calls the API again.
- This repeats (call, run, return) as often as the task needs. Cap it with a turn or budget limit.
- When Claude has what it needs, it answers with stop_reason end_turn and the loop stops.
transcript
$ python triage.py "why is checkout returning 502?"
turn 1 stop_reason=tool_use → get_pod_status(namespace="shop", app="checkout")
tool_result: 2/3 pods CrashLoopBackOff, last exit 137
turn 2 stop_reason=tool_use → get_pod_events(pod="checkout-7d9f")
tool_result: OOMKilled, limit 256Mi
turn 3 stop_reason=end_turn
Checkout pods are OOM-killed at the 256Mi limit; raise the memory limit or fix the leak.$ ./try-it.sh
Try it: a raw tool loop for a pod-status tool
- Run
pip install anthropicand exportANTHROPIC_API_KEYfrom your secret store (don't paste it into the script). - Save the loop from this lesson as
loop.py. Define one tool,{"name": "get_pod_status", "description": "List pods and their status in a Kubernetes namespace", "input_schema": {"type": "object", "properties": {"namespace": {"type": "string"}}, "required": ["namespace"]}}, and arun_toolthat returns canned JSON: two pods, one of them inCrashLoopBackOff. - Run
python loop.pywith the task "Which pod in namespace payments is unhealthy and why?" and printresp.stop_reasonon every step. You should seetool_use, thenend_turn. - Set
MAX_STEPS = 1and run again. Confirm that the script raises your guard error instead of returning a half answer. - Make
run_toolraiseTimeoutErroronce. Check that the model receives anis_errorresult and either retries or explains the failure.
Check yourself
5 questions. Original practice questions, not taken from the real exam.