Context and Tools
Agents and Tool Use

How the Agent Loop Works: Decide, Call, Observe, Respond

How the Agent Loop Works: Decide, Call, Observe, Respond
This post breaks down the core mechanics of AI agent systems by examining the four-stage loop: Decide, Call, Observe, and Respond. Through practical Python examples and common failure patterns, it provides a clear mental model for debugging agent logic, managing context windows, and building reliable tools without relying on heavy frameworks.

The first time I tried to explain an agent loop to a teammate I drew a circle on a whiteboard and labeled the four stages with arrows. Then I erased the circle and wrote the same four words in a straight line. The second version was clearer. Agents do not really go in circles; they move through a repeated sequence of decisions until they either finish the job or hit a limit you set.

That sequence is the entire subject of this post:

  1. Decide – the model examines the current messages and chooses whether to answer or call a tool.

  2. Call – your code executes the requested tool.

  3. Observe – the tool result is appended to the conversation.

  4. Respond – the model looks at the new information and either finishes or decides again.

Everything that makes an agent useful or dangerous lives inside those four steps. Today we will slow the loop down, look at each stage with real code, and examine the places it commonly fails. No new frameworks. Just the same Messages API and the same single-tool agent from the previous post, now examined under a brighter light.

As always:

Build the smallest useful version, then test what breaks.

Why the Loop Matters More Than the Tools

Most people start by writing more tools. I did the same. The more interesting failures, however, almost never come from a missing tool. They come from the loop itself:

  • The model keeps deciding to call tools long after it has enough information.

  • A tool returns an error and the model never observes it correctly.

  • The conversation history grows until earlier constraints disappear.

  • There is no clear exit condition, so the loop burns tokens until someone notices the bill.

Understanding the four stages lets you diagnose these problems instead of just adding more tools and hoping.

Stage 1: Decide

The decision happens inside the model. You send the current messages (plus the tool definitions) and the model returns either:

  • ordinary text content and a stop reason other than tool_use, or

  • one or more tool_use blocks and a stop reason of tool_use.

Here is the decision point in code, taken from the minimal agent we built last time:

Python

response = client.messages.create(
    model="claude-sonnet-5",  # verify current ID in official docs
    max_tokens=512,
    tools=TOOLS,
    messages=messages
)

messages.append({
    "role": "assistant",
    "content": response.content
})

if response.stop_reason != "tool_use":
    # The model has decided it can answer.
    final_text = "".join(
        block.text for block in response.content if block.type == "text"
    )
    return final_text

Two details are easy to miss:

  • You must keep the entire assistant content, not just the text or the tool blocks. Later model versions sometimes include additional block types; discarding them can produce API errors.

  • The decision is based only on what is currently in the messages list. If an earlier constraint has been truncated out of the context window, the model cannot decide on the basis of that constraint.

    Young female computer student focused, deep in thought, looking at code on a screen in a dimly lit lab.

I still occasionally watch a model “decide” to ignore a safety rule that was present in the first user message. When I dump the messages list I discover the rule is no longer there. The model did not rebel; the context simply ran out of room.

Stage 2: Call

Once the model has requested a tool, your code is responsible for executing it. This is the only stage that is entirely under your control.

Python

for block in response.content:
    if block.type == "tool_use":
        tool_name = block.name
        tool_input = block.input
        tool_use_id = block.id

        if tool_name not in TOOL_MAP:
            result = f"Error: unknown tool '{tool_name}'"
        else:
            try:
                result = TOOL_MAP[tool_name](**tool_input)
            except Exception as e:
                result = f"Error executing tool: {type(e).__name__}: {e}"

Three practical rules I follow here:

  1. Never let an exception escape the call stage. Turn every failure into a string the model can read.

  2. Log the tool name, the input, and the result (or error) before you continue.

  3. Keep the implementation of each tool as boring as possible while you are still learning the loop. Side effects belong in later posts.

The call stage is also the natural place to insert a human approval gate. We will do that in the next article. For now it is enough to notice that the model cannot force your code to run anything; you choose whether and how to execute the request.

Stage 3: Observe

Observation is simply the act of giving the tool result back to the model in the format it expects:

Python

tool_results = []
# ... collect results as shown above ...

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

Each item in tool_results must contain:

  • "type": "tool_result"

  • the matching "tool_use_id"

  • the "content" (usually a string, sometimes a list of content blocks)

If you send the wrong tool_use_id, or if you forget to wrap the results in a user message, the model receives incomplete information and the next decision becomes unreliable. Most of the “the agent ignored the tool result” bugs I have debugged turned out to be malformed observation steps.

A useful habit: after you append the tool results, print the length of the messages list and the latest tool result. Confirmation that the observation actually entered the conversation saves later guesswork.

Stage 4: Respond (or Decide Again)

After the tool result is observed, control returns to the model. It now has new information and must decide again: answer the user or call another tool. That is why the four stages feel like a loop. The “Respond” stage is really just the next “Decide” stage with richer context.

The only clean ways the loop ends are:

  • the model returns a stop reason other than tool_use, or

  • your code hits the max_iterations limit you set in advance.

There is no third option that is safe. Relying on the model to “just stop when it is finished” is how you get the seventeen-call runaway I described at the beginning of the previous post.

Putting the Four Stages Together

Here is the same loop annotated with the stage names:

Python

def run_agent(user_message: str, max_iterations: int = 5) -> str:
    messages = [{"role": "user", "content": user_message}]

    for iteration in range(1, max_iterations + 1):
        print(f"\n=== Iteration {iteration} ===")

        # Stage 1: Decide
        response = client.messages.create(
            model="claude-sonnet-5",
            max_tokens=512,
            tools=TOOLS,
            messages=messages
        )
        messages.append({"role": "assistant", "content": response.content})
        print(f"Stop reason: {response.stop_reason}")

        if response.stop_reason != "tool_use":
            # Stage 4: Respond (final answer)
            return "".join(
                block.text for block in response.content if block.type == "text"
            )

        # Stage 2: Call
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                tool_name = block.name
                tool_input = block.input
                print(f"Calling {tool_name} with {tool_input}")
                try:
                    result = TOOL_MAP[tool_name](**tool_input)
                except Exception as e:
                    result = f"Error: {e}"
                print(f"Result: {result}")
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result
                })

        # Stage 3: Observe
        messages.append({"role": "user", "content": tool_results})

    return "Stopped: max iterations reached."

When I am debugging a misbehaving agent I walk through these four labels in order. Most problems announce themselves clearly once you know which stage failed.

Three Loop-Level Failures Worth Testing

1. Decision Without Sufficient Context

Remove an important earlier message and watch the model decide to do something the original constraints would have forbidden. The failure is not in the model’s reasoning; it is in the messages list you supplied.

2. Call That Swallows Errors

Let the tool raise an exception and confirm the error string appears in the next model turn. If the exception crashes the Python process instead, the observation stage never happens and the loop is dead.

3. Observation That Loses the Tool Result

Intentionally send a tool result with the wrong tool_use_id. The model will often ignore the result or ask for the tool again. The log will show the mismatch immediately.

I keep a short checklist of these three cases and run it whenever I change the loop. It takes only a few minutes and has caught several regressions before they reached a client.

Practical Rules I Use for Every Agent Loop

  • Always set a hard max_iterations (or an equivalent token or time budget).

  • Log stop reason, tool name, input, and result on every iteration while developing.

  • Keep the entire assistant content when you append to messages.

  • Turn every tool exception into an observable string.

  • When the final answer looks wrong, dump the full messages list before you change the prompt.

These rules are deliberately mechanical. Mechanical rules survive contact with production traffic better than clever heuristics.

What This Post Does Not Cover

We have not yet added:

  • Human approval before side-effecting tools

  • Multiple tools that depend on one another

  • Streaming of partial decisions

  • Automatic context compaction when the window fills

  • Persistent memory across separate agent runs

Those topics belong to later articles. The goal today is a clear mental model of the four stages so that every subsequent improvement can be placed in the correct part of the loop.

Three young developers discussing a complex cycle diagram on a whiteboard at a busy tech conference.

Verification Checklist

  1. Run the annotated agent with a simple addition request and confirm you can point to each of the four stages in the log.

  2. Force a tool exception and confirm the error is observed by the model on the next turn.

  3. Set max_iterations=1 and confirm the loop stops with the safety message.

  4. Ask a question that needs no tool and confirm the model decides to answer on the first iteration.

  5. Intentionally corrupt a tool_use_id and confirm the model’s next decision becomes unreliable.

If all five steps behave as expected, you understand the loop.

Update Note

The Messages API shapes used in this post (tool_use, tool_result, stop reasons) have been stable for a long time. Model identifiers and the exact set of content block types can change. Always verify the current recommended model ID and any newer helper utilities in the official Anthropic documentation before you rely on specific behavior in production code.

Looking Ahead

The next post adds the missing safety piece: a human approval step before the agent is allowed to take an action that has real-world effects. Once that gate is in place, the same four-stage loop becomes something you can begin to trust with email, database writes, or external API calls.

Keep the annotated script. When a larger system later misbehaves, you will already know how to reduce the problem to a single decide-call-observe-respond cycle you can inspect by hand.

Build the smallest useful version, then test what breaks.

— Caleb Fletcher

Boise, Idaho

Updated · 2026-10-17 12:04
Feedback

No feedback yet — submit the first.

Submit feedback
© 2026 Context & Tools. All rights reserved. data-driven, published weekly ♥