Skip to content

05: Agent Loop

Now we finally need an agent.

Not because it is more advanced. Because the fixed workflow hit one specific problem:

The next step must depend on the previous tool result.

The Hangzhou trip has that shape. Before checking weather, you do not know that it rains in the afternoon. After seeing rain, the next step should search for indoor-friendly places. After places are chosen, the route can be estimated.

The path is not fully known at the start. It is chosen while the run unfolds.

Code First

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from agent_patterns_lab.patterns.react import run_react
from agent_patterns_lab.runtime import MockLLM, RunLimits, Tool, ToolRegistry, Tracer


def as_json(value: dict[str, Any]) -> str:
    return json.dumps(value, ensure_ascii=False)


def main() -> None:
    tracer = Tracer()

    def get_weather(args: dict[str, Any]) -> str:
        city = args["city"]
        date = args["date"]
        return as_json(
            {
                "city": city,
                "date": date,
                "forecast": "light rain after 15:00",
                "temperature_c": "18-23",
                "packing_hint": "umbrella and light jacket",
            }
        )

    def search_places(args: dict[str, Any]) -> str:
        return as_json(
            {
                "city": args["city"],
                "matches": [
                    "West Lake: best before the afternoon rain",
                    "China National Tea Museum: indoor-friendly and good for tea lovers",
                    "Hefang Street: local snacks and easy evening walk",
                ],
            }
        )

    def estimate_route(args: dict[str, Any]) -> str:
        places = args["places"]
        return as_json(
            {
                "route": places,
                "total_transit_minutes": 55,
                "note": "Keep West Lake first, move indoor after rain starts.",
            }
        )

    tools = ToolRegistry(
        [
            Tool(
                name="get_weather",
                description="Get a simple weather forecast for a city and date",
                handler=get_weather,
            ),
            Tool(
                name="search_places",
                description="Find travel places based on city, interests, and constraints",
                handler=search_places,
            ),
            Tool(
                name="estimate_route",
                description="Estimate travel time for a short list of places",
                handler=estimate_route,
            ),
        ]
    )

    model = MockLLM(
        [
            as_json(
                {
                    "type": "tool",
                    "tool": "get_weather",
                    "args": {"city": "Hangzhou", "date": "tomorrow"},
                }
            ),
            as_json(
                {
                    "type": "tool",
                    "tool": "search_places",
                    "args": {
                        "city": "Hangzhou",
                        "interests": ["tea", "local food", "easy walking"],
                        "constraint": "light rain after 15:00",
                    },
                }
            ),
            as_json(
                {
                    "type": "tool",
                    "tool": "estimate_route",
                    "args": {
                        "places": [
                            "West Lake",
                            "China National Tea Museum",
                            "Hefang Street",
                        ]
                    },
                }
            ),
            as_json(
                {
                    "type": "final",
                    "answer": (
                        "Plan: West Lake in the morning, China National Tea Museum after "
                        "the rain starts, then Hefang Street for snacks. Pack an umbrella, "
                        "a light jacket, and comfortable shoes."
                    ),
                }
            ),
        ]
    )

    out = run_react(
        model,
        task=(
            "Plan a relaxed one-day Hangzhou trip for tomorrow. "
            "I like tea, local food, and easy walking. Tell me what to pack."
        ),
        tools=tools,
        limits=RunLimits(max_steps=6),
        tracer=tracer,
    )

    print(out)
    trace_path = tracer.export_jsonl(Path(".traces") / "21_react_loop.jsonl")
    print(f"[trace] {trace_path}")


if __name__ == "__main__":
    main()

Run:

uv run python examples/21_react_loop.py

Expected output:

Plan: West Lake in the morning, China National Tea Museum after the rain starts, then Hefang Street for snacks. Pack an umbrella, a light jacket, and comfortable shoes.
[trace] .traces/21_react_loop.jsonl

What An Agent Loop Is

An agent loop is not a longer prompt. It is control flow:

flowchart TD
  S["Current state: task + messages + tool results"] --> M["Model chooses next action"]
  M -->|tool| T["Python calls tool"]
  T --> O["Observation appended to state"]
  O --> S
  M -->|ask| Q["Ask user"]
  M -->|final| F["Return final answer"]
  S --> L["Step / cost / time limits"]
  L -->|limit hit| X["Stop"]

Each round:

  1. Python sends the current messages to the model.
  2. The model returns an action: get_weather, search_places, estimate_route, or final.
  3. Python validates and executes the action, then appends the observation.
  4. If there is no final, the loop continues.

The model chooses the next step. Python executes, records, limits, and stops.

Why This Looks Like An Agent

Look at the trace:

Round Model action Python observation Why the next step changes
1 get_weather Light rain after 15:00 Afternoon should not be all outdoors
2 search_places Tea Museum fits rain, Hefang Street fits snacks Search is now constrained by weather and interests
3 estimate_route About 55 minutes total transit Route stays relaxed
4 final Enough information Stop

The important part is not "three tools were called". The important part is that round two changes because of round one.

That is the boundary between workflow and agent loop:

Shape Where the path comes from
Workflow Python predefines it
Agent Loop Model chooses each step from state

This Is The Door To ReAct

This example is ReAct-style:

action -> observation -> next action -> next observation -> final answer

This project writes actions as JSON instead of free-form text:

{"type": "tool", "tool": "get_weather", "args": {"city": "Hangzhou", "date": "tomorrow"}}

That lets Python parse, validate, execute, and trace the loop.

New Problems The Loop Creates

Agent loops solve uncertain next steps. They also create new risks:

Risk Example What you need
Infinite loop Keeps checking weather and never answers max_steps, timeout, stagnation checks
Wrong tool Checks places when it should estimate route Tool descriptions, allowlists, trace
Fake observation Says "sunny" without calling the tool Trust only Python-written tool messages
Weak final answer Looks plausible but misses constraints Maker-Checker, Voting, CoVe
Longer task One itinerary becomes many subtasks Planner-Executor-Replanner
Real-world side effects Booking, payment, cancellation Policy, Guardrails, HITL

So the agent loop is not the end. It opens the door to the rest of the design patterns.

What To Remember

If the steps are fixed, use a workflow.

If the next step must depend on the previous observation, use an agent loop, and keep the loop under Python's control.

Next: read the deeper ReAct pattern page, then use Choose a Pattern as the map.