Skip to content

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.

Ընտրեք տեքստը՝ հայերեն թարգմանությունը տեսնելու համար։Highlight text to see Armenian. Machine-assisted translation; English is the source of truth.

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.

messages + toolsmodel callstop_reason?run toolsfinal answerend_turntool_usetool_result blocksyour guards: max steps, budget, wall clock → stop with an error, not a guess
The model decides whether to call tools; your code runs them and also owns the guards that end a loop that won't end on its own.

One turn, step by step

  1. Call the Messages API with messages and tools.
  2. Append the assistant response to messages, unchanged.
  3. Read stop_reason. If it’s tool_use, run every tool_use block in the response and reply with one tool_result per block, matched by tool_use_id.
  4. 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 matching tool_result blocks, repeat.
  • end_turn is the normal exit. tool_use and pause_turn mean “continue”. max_tokens, model_context_window_exceeded and refusal mean the turn was cut short.
  • Report tool failures with is_error: true and a useful message.
  • Always add your own caps (steps, budget, time), and fail loudly when they fire.

See it in action

The agentic loop, one turn at a time
tool_usetool_resultend_turnyour promptClaudeMessages APIyour code runs the tooltool_use → tool_resultfinal answerstop_reason: end_turn

1/5 You send a prompt, plus the tool definitions, to the Messages API.

all steps
  1. You send a prompt, plus the tool definitions, to the Messages API.
  2. Claude replies with stop_reason tool_use: it wants a tool run. Your code runs the tool; the model never executes anything itself.
  3. Your code sends the result back as a tool_result block in the next user turn and calls the API again.
  4. This repeats (call, run, return) as often as the task needs. Cap it with a turn or budget limit.
  5. When Claude has what it needs, it answers with stop_reason end_turn and the loop stops.
an agent loop, traced · illustrative session
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

  1. Run pip install anthropic and export ANTHROPIC_API_KEY from your secret store (don't paste it into the script).
  2. 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 a run_tool that returns canned JSON: two pods, one of them in CrashLoopBackOff.
  3. Run python loop.py with the task "Which pod in namespace payments is unhealthy and why?" and print resp.stop_reason on every step. You should see tool_use, then end_turn.
  4. Set MAX_STEPS = 1 and run again. Confirm that the script raises your guard error instead of returning a half answer.
  5. Make run_tool raise TimeoutError once. Check that the model receives an is_error result and either retries or explains the failure.

Check yourself

5 questions. Original practice questions, not taken from the real exam.

  1. Your incident bot calls the Messages API and gets back stop_reason "tool_use" with two tool_use blocks: get_pod_logs and get_events.

    1. What should the next request contain?

  2. A nightly cleanup agent built on a raw Messages API loop gets a response with stop_reason "max_tokens". The last content block is a tool_use for kubectl scale whose input was cut off.

    2. What should the loop do?

  3. Your team puts a tool-using agent behind a Slack command for on-call. In testing, one run called the same search tool 40 times before anyone noticed.

    3. Which two stop conditions should your code enforce itself, instead of relying on the model? (Choose two.)

    Choose 2.

  4. Your agent's query_prometheus tool raises ConnectionError because the Prometheus endpoint is down.

    4. How should the loop report this to the model?

  5. Your research agent uses a server-side tool (web search). A response comes back with stop_reason "pause_turn".

    5. What does a correct loop do?

Answer every question to check.