Part 3 · Letting it act

Build with LLMs · ~8 min

The agent loop

Build the while-loop that turns tool calling into an agent, and the budgets that stop it.

An agent is a while-loop

Lesson 06 ended after one round trip: the model asked for a tool, you ran it, the model answered. An agent is that, in a loop, until the model stops asking.

That is the whole definition. Not planning, not memory, not reasoning — a while around tool calling.

  • Send the messages.
  • Did the model ask for a tool? If yes, run it, append the result, send again.
  • If no, you are done. Return the answer.

Everything else people call "agentic" is this loop plus better tools, better prompts, or better budgets.

import anthropic, time

client = anthropic.Anthropic()

def run_agent(task, tools, handlers, max_turns=10, max_seconds=120):
    messages = [{"role": "user", "content": task}]
    deadline = time.monotonic() + max_seconds

    for turn in range(max_turns):
        if time.monotonic() > deadline:
            return "Stopped: out of time."

        reply = client.messages.create(
            model="claude-sonnet-5", max_tokens=2048, tools=tools, messages=messages
        )
        messages.append({"role": "assistant", "content": reply.content})

        # no tool requested means the model is finished - this is the normal exit
        if reply.stop_reason != "tool_use":
            return "".join(b.text for b in reply.content if b.type == "text")

        results = []
        for block in reply.content:
            if block.type != "tool_use":
                continue
            try:
                output = handlers[block.name](**block.input)
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": str(output)})
            except Exception as e:
                # errors go back as readable text, so the model can adjust and retry
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": f"Tool failed: {e}", "is_error": True})

        messages.append({"role": "user", "content": results})

    return "Stopped: hit the turn limit."

Two details that are easy to get wrong. The model can return several tool_use blocks in one response, so loop over the content and send all the results in a single user turn — a missing tool_result is an API error. And every exit path returns something; there is no branch where the loop just falls off the end.

An agent is not a smarter model. It is the same model, called repeatedly, with the results of its own last request added to what it can see.

Stopping, and the three ways it fails

The normal exit is stop_reason == "end_turn" — the model wrote an answer and asked for nothing. Most runs end there. The interesting cases are the ones that do not.

Spinning. The model calls the same tool with the same arguments over and over. This happens when the result does not change what it believes. A search returns nothing; the model tries the same search again, because from its point of view nothing has ruled that approach out.

Fix it by changing what it sees. Track a hash of (tool name, arguments). On a repeat, do not run the tool — return a result that says so: You already called search with these arguments and it returned nothing. Try different terms or a different tool. That is new information, and the model moves on.

Drifting. Twelve turns in, the agent is refactoring a helper it noticed on the way and has forgotten you asked it to fix a test. The original instruction is now thousands of tokens back and the recent turns are all about something else.

Fix it by keeping the objective near the end of the context, not just at the start. Restate the goal in the loop — a short line appended to the tool results every few turns is enough. Narrower tools help too: an agent that cannot edit unrelated files cannot wander into them.

Running out of context. The window fills, and the call fails or the earliest turns get dropped. This is the one that sneaks up on you, because turn one looks nothing like turn twenty.

FailureWhat you seeThe fix
SpinningSame tool, same arguments, repeatedlyDetect repeats, return a result that rules the approach out
DriftingUseful work, wrong taskRestate the goal late in the context; narrow the tools
Out of contextError, or the agent forgets the original askCompact old turns; shrink tool outputs; use a sub-agent

Budgets are the real control

Prompts are guidance. Budgets are enforcement. Set all three, and set them in your loop where the model cannot argue with them:

  • Max turns. The blunt one. Ten to twenty-five covers most tasks. If a task genuinely needs sixty, split it.
  • Max tokens spent. Sum usage.input_tokens and usage.output_tokens across the run and stop at a ceiling. This catches the run that is not long but is enormous.
  • Max wall-clock. A tool that hangs burns minutes without burning turns or tokens. Only a deadline catches that.

When a budget trips, return a partial answer rather than an error. Ask the model for its best summary of what it found, or hand back the tool results you have. A stopped agent that says "here is what I learned, here is what I did not finish" is useful. A stopped agent that raises is not.

Context is the cost, and sub-agents are the fix

The API is stateless. Every turn you resend the entire conversation — the system prompt, the tool schemas, every previous message, every tool result. Turn twenty pays for turns one through nineteen again.

So the cost of a run grows roughly with the square of its length. Twenty turns is not twice ten turns; it is closer to four times. Two things follow.

Prompt caching cuts the reread. The stable front of the conversation — system prompt, tool schemas, early turns — can be cached, so you pay much less for resending it. It changes the constant, not the shape of the curve, but on a long loop that is a large saving for one config change.

Compaction cuts the length. Old tool results are usually the bulk of the context and usually the least useful part of it. Options, cheapest first:

  • Truncate tool output at the source. A tool that can return 50,000 rows should return 50 and say how many were dropped.
  • Drop the bodies of old tool results, keep the fact that the call happened. Turn 3's full file contents rarely matter at turn 18.
  • Summarise the run so far into a short note and start a fresh message list from it. This is what "compacting" means in practice.

When to use a sub-agent

Sometimes the answer is not to compress the context but to keep it out. A sub-agent is a second loop, with its own fresh message list, its own tools, and its own budgets. It does one job and returns a short result. The main loop sees only that result.

The test is what the sub-task leaves behind. If finding one answer means reading forty files, that is forty file bodies in your context to learn one line. Push it down.

SituationDo this
A search that reads a lot to conclude a littleSub-agent; return the conclusion only
Three independent lookupsSub-agents in parallel; each returns a summary
A step whose intermediate detail is never needed againSub-agent
Steps that build on each other's full outputKeep in one loop; splitting loses what the next step needs
A quick two-step taskKeep in one loop; the overhead is not worth it

The cost is real: a sub-agent cannot see the main conversation, so its instructions have to be self-contained, and anything it does not report back is lost. That is the same trade you make when you delegate to a person. Use it when the detail is the noise, not the answer.

Your win

  • Write the model to tool to result to model loop from memory.
  • Terminate on stop_reason of end_turn, and on hard budgets either way.
  • Spot the three failure modes: spinning, drifting, running out of context.
  • Expect cost to grow faster than turns, because history is resent every time.
  • Split work into a sub-agent when the context is full of detail the main loop does not need.

Retrieval practice — recall, don’t peek

  1. Question 1

    What normally ends the agent loop?

  2. Question 2

    Ten turns into a loop, why is turn ten more expensive than turn one?

  3. Question 3

    Your agent calls the same failing search three times in a row. What is the fix?

  4. Question 4

    Which budget protects you from a loop that is slow rather than long?

  5. Question 5

    When is a sub-agent the right move?

Go deeper