---
title: "Build the Agent Loop Yourself, Once"
author: "Harry"
published: 2026-10-31T04:48:08.849Z
updated: 2026-10-31T05:07:29.329Z
category: "Building with AI"
tags: ["prompts-guide"]
canonical: "https://fxblog.dev/posts/build-the-agent-loop-yourself"
---

# Build the Agent Loop Yourself, Once

## Key takeaways

- Every agent product runs the same loop: the model asks for a tool, your code runs it, and the result goes back.
- A tool description is a prompt, so it should say when to call the tool; a tool result is context, so keep it short.
- Return all tool results from one turn in a single user message, one tool\_result per tool\_use.
- Cap the loop, return errors as results, gate destructive tools behind a person and log every call.
- Forced tool\_choice returns 400 on Opus 5.5, Sonnet 5.5 and Fable 5.1; use auto, name the tool and check a call was made.
- Use more than one agent only when a single loop hits a wall of context, parallelism or conflicting roles.

> **TL;DR** When an agent stalls or loops, you cannot fix what you cannot see. Every agent product runs the same agent loop: the model asks for a tool, your code runs it, the result goes back. Write that loop once by hand, about a dozen lines, then harden it: cap the turns, return errors as results, put a person in front of destructive tools, and log every call.
>
> **Do this:** Type the eleven-line loop below by hand, then cap it at 5 to 15 turns before anyone else runs it.

[Part 1](01-api-basics.md) left the test-case generator writing one case per call. It could not run a test, so it could not check its own work against the orders suite. It needs a tool, and a loop around the tool.

Eleven lines of Python. That is the engine under every agent product you have used. Once you have typed them yourself, an agent that hangs is no longer a mystery: you can say which message it is waiting for.

**This series, in six parts:**

1. **Your Prompt Worked in Chat. Why Is the LLM API Different?**: the call, and what comes back.
2. **Build the Agent Loop Yourself, Once** (you are here): tools, and the loop every agent runs.
3. **Did Your Prompt Change Help? Prove It with an Eval**: a number for every change.
4. **RAG That Works: Why Retrieval Matters More Than the Prompt**: answers from your own docs.
5. **Your LLM Demo Works. What Every Call Needs in Production**: cost, failures, logs, injection.
6. **Your LLM Proxy Retries the Wrong Errors**: rate limits, retries and streams.

## The model asks; your code acts

You describe a tool with a name, a description and a schema. The model never runs anything. It answers "call `run_test_suite` with this path", your code runs it, and the result goes back in the next message.

Here is the generator's first tool, in the SDK's decorator form:

```python
from anthropic import beta_tool

@beta_tool
def run_test_suite(path: str) -> str:
    """Run the Playwright suite at the given path and return the summary.

    Args:
        path: Directory of specs to run, e.g. tests/api/orders.
    """
    ...  # your implementation
```

Look at the docstring. It says what the tool does, not _when_ to call it, and the model decides from that text alone. The description is a prompt, so add the when. The result is context too: the whole test log makes the next decision worse than a short list of what failed.

![The agent loop: send history and tools; if the model asks for a tool, run it, append the result and send again; when it answers without a tool, or the cap is hit, the loop ends](https://fxblog.dev/media/01a19049-02ca-7720-9662-a0d2f60d394f/1600)

_The model decides; your code acts._

## The loop, by hand

This is the guide's loop, unchanged:

```python
messages = [{"role": "user", "content": task}]
while True:
    resp = client.messages.create(model=MODEL, max_tokens=16000,
                                  tools=TOOLS, messages=messages)
    if resp.stop_reason != "tool_use":
        break                                    # final answer
    messages.append({"role": "assistant", "content": resp.content})
    results = [{"type": "tool_result", "tool_use_id": b.id,
                "content": run_tool(b.name, b.input)}      # YOUR code
               for b in resp.content if b.type == "tool_use"]
    messages.append({"role": "user", "content": results})
```

Read it for what it leaves out. `while True` never stops on its own. One exception in `run_tool` kills the whole run. Nobody approves anything.

> **Tip**
>
> The model can ask for several tools in one turn. Send every `tool_result` back in **one** user message, each matched to its `tool_use` id, as the list above does. Split across messages, it teaches Claude to stop making parallel calls.

## Four changes before anyone else runs it

1. **Cap the loop** at 5 to 15 turns, then stop and hand the task to a person.
2. **Return errors as results**: a `tool_result` with `is_error: true`. The model reads the error and recovers; a crash teaches it nothing.
3. **Gate destructive tools** (delete, send, deploy) behind human approval.
4. **Log every tool call.**

> **Warning**
>
> Forcing a tool with `tool_choice` `any` or `tool` returns 400 on Opus 5.5, Sonnet 5.5 and Fable 5.1. Use `auto`, name the tool in the prompt, and set `strict: true` on the tool for schema-valid arguments. `auto` does not promise a call, so check that one was made.

I'd write the loop once to understand it, then switch to the SDK's tool runner, `client.beta.messages.tool_runner`: the same loop, with hooks for approval, errors and an iteration cap that you no longer maintain.

## When one loop is not enough

Reach for more agents only when one loop hits a real wall: context size, needed parallelism, or roles that conflict.

| Shape               | How                                                                                     | Fits                          |
| ------------------- | --------------------------------------------------------------------------------------- | ----------------------------- |
| Planner and workers | A strong model splits the job; cheap workers run shards in parallel; the planner merges | "Process 200 files"           |
| Writer and verifier | Generation and adversarial review in separate contexts                                  | A wrong answer is costly      |
| Specialist roster   | Narrow prompt and narrow tools per agent                                                | Beats one agent with 40 tools |

Agents share files and structured reports, never raw conversation histories, and every delegated task carries its own full brief. In the guide's words: if a plain `for` loop over an API call does the job, that's not a multi-agent system, and that's a compliment.

## Try it

```text
Write a tool definition for <a tool my app needs>: name, a
description that says WHEN to call it, and a strict JSON schema.
Then list three results it could return and rewrite each to be as
short as possible while still useful to the model.
```

The test-case generator now runs the orders suite and reads its own failures. Then you edit its system prompt, and the next run's cases look better. Better, or only different? Part 3, Did Your Prompt Change Help?, answers that with a number.

## Read this with your AI

```text
Read the post "Build the Agent Loop Yourself, Once".
1. Summarise it in 5 bullets for a <your role>.
2. Ask me 3 questions to check I understood; wait, then correct me.
3. Review my agent loop (below) against the four changes and the
   one-message rule. What breaks first in production?
<paste>
```

> **Success**
>
> You know what every agent product is doing under the hood. Next: **Did Your Prompt Change Help? Prove It with an Eval**.

## FAQ

### Should I write my own loop or use the SDK's tool runner?

Write it once to understand it, then use the SDK tool runner, which is the same loop with hooks for approval, error handling and an iteration cap.

### What happens when a tool fails?

Return the error as a tool result marked is\_error. The model reads it and usually recovers; crashing the loop teaches it nothing.
