Skip to content
fx-blog

Build the Agent Loop Yourself, Once

Tool use and the agent loop in eleven lines of Python, the four changes that harden it, and when one loop stops being enough.

Harry4 min readIntermediate
  • Intelligence
MarkdownSave

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.
Contents

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 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:

python03-api/claude-api.md
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

The loop, by hand

This is the guide's loop, unchanged:

python03-api/production-patterns.md
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.

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.

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

textPaste into your AI
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

textPaste into your AI
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>

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.

Sources

  1. AI Practices Guide: Production patterns
  2. AI Practices Guide: Claude API
  3. Claude docs: Tool use
  4. Anthropic: Building effective agents

Was this useful?

Only you and the owner see your answer and rating. Send private feedback

Enjoyed it? Support fx-blog with a coffee.

Comments

Oldest first

No comments yet. Start the conversation.

You sign in first; what you typed stays here. · 0 / 2,000