Index Ai Academy
Phase 1 · Building with LLMs · Tools & agents

The agent loop

Model → tool → result → model, until the task is done.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. From one round trip to a loop
  2. Safety limits
  3. A loop that always ends
  4. Why long agent runs get expensive
  5. You don't always write the loop yourself
  6. Stop reasons that aren't success
  7. 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 most MAX_TURNS times.
  • 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_reasonMeaningWhat your loop does
tool_useThe model wants one or more toolsRun them, send the results, continue
end_turnThe model finished its answerStop and return the text
max_tokensThe reply hit your max_tokens limit partway throughStop and report it, or raise the limit
refusalThe model declined for safety reasonsStop 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.

Quick check
What does this print?
replies = ["tool_use", "tool_use", "end_turn"]
turns = 0
for stop in replies:
    turns += 1
    if stop == "end_turn":
        break
print(turns)
  1. 3
  2. 0
  3. 1
  4. 2
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.

Try it now

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.

Quick check
In the stepper, why does each tool turn add two messages?
  1. One for the model asking for the tool, one for your code's result
  2. One for the question and one for the final answer
  3. The API always duplicates messages
  4. Because the script has two entries
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.

Worked example

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"}))
It runs right here in your browser, with nothing to install.

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.

Worked example

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))
In the lesson you can change this and run it.

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.

Quick check
Your agent loop only stops when stop_reason == "end_turn". What can go wrong?
  1. A reply cut off by max_tokens (or a refusal) isn't a tool request, so the loop keeps calling the model
  2. The model can't use tools
  3. Tool results are sent twice
  4. Nothing: end_turn is the only way to finish
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.

Quick check
Which tool should need a person's approval before the agent can run it?
  1. search_help_center
  2. pause_campaign
  3. get_campaign
  4. list_campaigns
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

Create a free account

Try a quiz question

What makes a program an "agent" rather than a prompt chain?

  1. The model decides the next step (which tool, or whether it's done) in a loop
  2. It uses more than one prompt, each building on the output of the one before
  3. It uses the most expensive model, because only that one can call tools
  4. It has a system prompt that tells the model it's an agent and what it may do

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson