The agent loop
Model → tool → result → model, until the task is done.
Create a free account Try part of it here
Why it matters
An agent is a model using tools in a loop. It looks something up, reads the result, decides what to do next, and repeats until the job is done. That's how an assistant can answer "which active campaign has the best return on ad spend?" First it has to find the active campaigns. Then it fetches each one's numbers. Then it compares them. The loop itself is about 15 lines of code. The judgment is in the safety limits around it.
What you'll be able to do
- Turn a single tool round trip into a loop that runs until the model is done
- Use stop_reason to decide whether to continue
- Add the guardrails every agent needs: a turn limit, approval for risky tools, and a log of steps
- Explain why agents get slower and more expensive with every turn
What it covers
- From one round trip to a loop
- Safety limits
- A loop that always ends
- Why long agent runs get expensive
- You don't always write the loop yourself
- Stop reasons that aren't success
- Keep tool results small
From one round trip to a loop
In the last lesson, your code handled one tool request, then asked for the final answer. Real tasks often need several steps, and the model can't know in advance how many.
Think of a detective. They don't plan every step on day one. They follow one clue, read what it says, then decide on the next clue. They stop when the case is solved.
So your code keeps going for as long as the model keeps asking for tools:
messages = [{"role": "user", "content": task}]
for turn in range(MAX_TURNS):
response = call_claude(messages)
messages.append({"role": "assistant", "content": response["content"]})
if response["stop_reason"] != "tool_use":
break # the model is done (or stopped for another reason)
results = [run(block) for block in response["content"] if block["type"] == "tool_use"]
messages.append({"role": "user", "content": results})
That's the whole agent loop. Line by line:
for turn in range(MAX_TURNS)repeats at mostMAX_TURNStimes.- Each turn, your code sends the whole conversation so far and adds the model's reply to it.
- If the model didn't ask for a tool, the loop stops.
- Otherwise, your code runs every requested tool and adds the results as the next message.
The model plans each next step from everything it has seen so far. That's why the same few lines can handle a one-step lookup and a six-step investigation.
Reading stop_reason
Every reply has a stop_reason. It tells you why the model stopped writing:
| stop_reason | Meaning | What your loop does |
|---|---|---|
tool_use | The model wants one or more tools | Run them, send the results, continue |
end_turn | The model finished its answer | Stop and return the text |
max_tokens | The reply hit your max_tokens limit partway through | Stop and report it, or raise the limit |
refusal | The model declined for safety reasons | Stop and handle it |
Continue only on tool_use, and treat everything else as "stop". Here's why. A loop that only checks for end_turn runs forever when a reply is cut off by max_tokens.
replies = ["tool_use", "tool_use", "end_turn"] turns = 0 for stop in replies: turns += 1 if stop == "end_turn": break print(turns)
Show the answer
3
turns goes up before the check, so it counts all three and prints 3: two tool_use replies, then end_turn, which stops the loop. An agent loop works the same way: it keeps going while the model asks for tools.
A fake model replays a script so you can see the shape. Each tool turn adds two messages. The loop ends when the reply isn't a tool request, and it prints 3 calls, 6 messages.
Show the answer
One for the model asking for the tool, one for your code's result
The model's turn ("call list_campaigns") and your reply with the tool result ("result of list_campaigns"). Three model calls plus the first user message: 1 + 2 + 2 + 1 = 6 messages.
Safety limits
An agent decides its own next step. So the code around it has to set limits. There are four.
1. A turn limit. Models can get stuck. They retry a failing tool, or search in circles. Cap the number of model calls (5 to 20 is typical). When you hit the cap, return a clear "stopped" result.
2. Approval for actions with side effects. A side effect is a change in the real world. Reading data is low-risk. Pausing a campaign, sending an email or changing a budget is not. Keep a list of tools that need a person's OK. When the model asks for one, ask the person first. If they say no, tell the model with an error result. Don't just skip it silently:
NEEDS_APPROVAL = {"pause_campaign", "send_email"}
if block["name"] in NEEDS_APPROVAL and not approve(block):
result = {"type": "tool_result", "tool_use_id": block["id"], "is_error": True,
"content": "The user declined this action."}
3. A log of every step. Record each tool call and its input. When an agent gives a strange answer, the log of what it looked at is how you find out why.
4. Small tool results. Every turn sends the entire conversation again, including every tool result so far. A tool that returns 5,000 tokens of raw data makes every later turn slower and more expensive. Return only the fields the model needs.
Watch out: cost grows faster than the number of turns. Turn 10 re-sends turns 1 to 9. Prompt caching, covered later, cuts the cost of that repeated start a lot.
A loop that always ends
Every path through this loop ends, whatever the model does. The model here is scripted to get stuck, so that you can see the turn limit do its job.
def stuck_model(messages): return {"stop_reason": "tool_use", "tool": "search", "input": "same query"} def run(model, max_turns=5): messages = ["Find the refund policy"] for turn in range(1, max_turns + 1): reply = model(messages) if reply["stop_reason"] == "end_turn": return f"done in {turn} turns" if reply["stop_reason"] != "tool_use": return f"stopped: {reply['stop_reason']}" messages.append("tool result") return f"gave up after {max_turns} turns" print(run(stuck_model)) print(run(lambda m: {"stop_reason": "max_tokens"})) print(run(lambda m: {"stop_reason": "end_turn"}))
Without the limit, the first call would run until your budget ran out. Each outcome also returns a clear message rather than crashing, so your code can tell the user what happened.
Why long agent runs get expensive
Say the task and instructions take 1,000 tokens. Each turn adds 500 more: the model's request plus your tool result. Turn 1 sends 1,000 tokens. Turn 2 sends 1,500, because it includes turn 1. Turn 3 sends 2,000, and so on.
Over five turns, that's 1,000 + 1,500 + 2,000 + 2,500 + 3,000 = 10,000 tokens. Only 3,000 of them are new by the end. The rest are repeats. In Python:
base, per_turn = 1000, 500 sent = [base + per_turn * k for k in range(5)] print(sent) print(sum(sent))
It prints [1000, 1500, 2000, 2500, 3000], then 10000. Double the turns to 10 and the total more than triples, to 32,500. That's why small tool results and a turn limit matter.
Show the answer
A reply cut off by max_tokens (or a refusal) isn't a tool request, so the loop keeps calling the model
Continue only on tool_use, and stop on everything else. And always have a turn limit.
You don't always write the loop yourself
The Anthropic Python SDK includes a tool runner that runs this loop for you. You write ordinary Python functions with type hints, labels such as campaign_id: str that say what kind of value each one is. It builds the schemas, calls the tools and feeds back the results:
import anthropic from anthropic import beta_tool client = anthropic.Anthropic() @beta_tool def get_campaign(campaign_id: str) -> str: """Get one campaign's metrics by id. Call this when the user asks about a specific campaign. Args: campaign_id: Campaign id, e.g. cmp_101. """ return json.dumps(fetch_campaign(campaign_id)) runner = client.beta.messages.tool_runner( model="claude-opus-5-5", max_tokens=4000, tools=[get_campaign], messages=[{"role": "user", "content": "How is cmp_104 doing?"}], ) for message in runner: print(message)
The @beta_tool line above the function is a decorator: it turns your function into a tool definition. The text in triple quotes becomes the tool's description.
Still, it's worth writing the loop by hand once (you will, below). Then you know what the runner does, where to add approval steps and what to log. Hosted options such as Claude Managed Agents go further. They run the loop and the tools on Anthropic's servers.
Show the answer
pause_campaign
Pausing a campaign changes something in the real world. The other three only read data, which is low-risk.
Stop reasons that aren't success
Looping until end_turn isn't enough. A reply that stops at max_tokens was cut off, and it may never reach end_turn, so your loop spins forever. Handle each stop reason on purpose, and keep a limit on turns.
Keep tool results small
A huge raw API response fills the context and costs tokens on every later turn. Return only the fields the model needs for the task.
Practice what you just read
- 5 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
What makes a program an "agent" rather than a prompt chain?