AI Atlas
EN TR
Intermediate · ~2 min read #agent-loop #tool-use #react

Agent Loop

The tool-use loop

The core agent cycle in which the model calls a tool, the result is fed back, and these turns repeat until a stop condition is met.

THE TOOL LOOP · TURN BY TURNUSERfix the testsASSISTANTtool_use: bash(npm test)USERtool_result: 3 failedASSISTANTtool_use: edit(auth.ts)USERtool_result: okASSISTANTend_turn: fixedHARNESS CHECKSstop_reason · turn captool_userun → append → repeatend_turnfinal answer, loop endsmax_turnscap hit, force stopthe full history is resent every turnuntil the model stops asking for tools or the budget runs out
Definition

At the heart of every agent is a simple loop:

1. The harness sends the conversation history and tool definitions to the model. 2. The model either writes a final answer or emits one (or several) tool calls. 3. The harness runs the tool and appends the result to the history as a tool_result. 4. Go back to step 1.

In the Anthropic API you read this from stop_reason: tool_use means the model is waiting on a tool; end_turn means it's done. Claude Code's docs describe the same loop as three phases: gather context, take action, verify results — repeated as many times as needed.

The academic root of the idea is ReAct (Yao et al., 2022, "ReAct: Synergizing Reasoning and Acting in Language Models"): the model interleaves reasoning steps and actions, and each action's observation feeds the next thought. Today's APIs have made this native: thinking blocks and tool_use blocks arrive in the same response.

A loop may never stop on its own, so stop conditions are mandatory: the model finishing, a maximum turn count, a token/cost/time budget, an unrecoverable error, or the user interrupting. Remember the API is stateless: the full history is resent every turn, so each call gets a little bigger as turns pile up.

Analogy

Like feeling your way to a light switch in a dark room. You take a step (act), touch the wall (observe), think "switches are usually next to the door" (reason), and take the next step accordingly. Once you find it, you stop.

But a sensible person also sets a rule: "if I haven't found it in ten steps, I'll turn on my phone's flashlight." That rule is the loop's max turns limit.

Real-world example

User: "Fix the tests." The loop runs like this:

- Turn 1: The model asks for bash("npm test") → the harness runs it → result: 3 tests failing. - Turn 2: The model asks for read("src/auth.ts") → file contents come back. - Turn 3: The model calls edit(...) to fix the bug → "ok". - Turn 4: The model runs bash("npm test") again → all green. - Turn 5: The model asks for no tools and writes "Fixed 3 tests, the cause was…". stop_reason = end_turn → the loop ends.

Had the model gone down the wrong path and called the same tool for 25 turns, the harness's turn cap would have cut the loop off.

A deeper look
THE AGENT LOOPGOALnot done?PLANwhat to do?ACTcall a toolOBSERVEread resultREFLECTcloser to goal?repeats until goal is met or budget runs out
Code examples
Anthropic API · hand-written loop with a turn cap python
import anthropic

client = anthropic.Anthropic()
MAX_TURNS = 20

tools = [{
    "name": "run_command",
    "description": "Runs a shell command in the project folder and returns its output",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
    },
}]
messages = [{"role": "user", "content": "Run the tests and fix them"}]

for turn in range(MAX_TURNS):
    response = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=16000,
        tools=tools,
        messages=messages,
    )
    # Append the whole response (text + tool_use blocks)
    messages.append({"role": "assistant", "content": response.content})

    if response.stop_reason != "tool_use":
        break  # end_turn, max_tokens, refusal… → leave the loop

    results = []
    for block in response.content:
        if block.type == "tool_use":
            try:
                output = run_tool(block.name, block.input)  # your code
                results.append({"type": "tool_result",
                                "tool_use_id": block.id,
                                "content": output})
            except Exception as err:
                results.append({"type": "tool_result",
                                "tool_use_id": block.id,
                                "content": str(err),
                                "is_error": True})
    # All results go back in a single user message
    messages.append({"role": "user", "content": results})
else:
    print("Turn cap reached, loop stopped")
OpenAI Agents SDK · the SDK runs the loop python
from agents import Agent, Runner, function_tool, MaxTurnsExceeded


@function_tool
def get_order_status(order_id: str) -> str:
    """Return the shipping status for an order number."""
    return lookup_order(order_id)  # your code


agent = Agent(
    name="Support",
    instructions="Answer order questions using the tools.",
    tools=[get_order_status],
)

try:
    # max_turns defaults to 10; exceeding it raises
    result = Runner.run_sync(agent, "Where is TR-9921?", max_turns=6)
    print(result.final_output)
except MaxTurnsExceeded:
    print("The agent couldn't finish in 6 turns")
When to use
  • Tasks where the number of steps isn't known up front — debugging, research, code changes
  • When each step's result decides the next one (observe → decide → act)
  • When the model must verify its own output — run tests, read results, try again
  • When writing your own harness — this loop sits under every agent framework
When not to use
  • Jobs that finish with a single tool call — one turn is enough, don't build a loop
  • Fixed-step workflows — sequential, code-driven calls are more predictable
  • UIs with strict latency limits — every turn is another model call and another wait
Common pitfalls

No stop condition

If the model can't reach an answer it may call the same tool again and again. Always set a turn cap and, if possible, a token, cost or time budget; when the cap is hit, tell the user clearly what happened.

Missing or scattered tool_results

Every tool_use block the model emits needs a matching tool_result; otherwise the API rejects the request. Return the results of parallel calls together in a single user message.

Crashing the loop on a tool error

When a tool fails, don't swallow the exception and end the loop — send the error back to the model with is_error: true. The model will often try another route.

Appending only the text to history

If you keep only the text from a response, the tool_use and thinking blocks are lost and the next turn breaks. Append the whole response.content.