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:
- Python sends the current
messagesto the model. - The model returns an action:
get_weather,search_places,estimate_route, orfinal. - Python validates and executes the action, then appends the observation.
- 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.