Agent Loops
The ReAct pattern: reason, act, observe, loop. With stopping conditions, budgets and safety.
What is an agent loop?
An agent loop is the cycle: LLM reasons → decides an action (tool call) → executes → observes result → reasons again → ... → final answer. The ReAct pattern (Reason + Act) is the canonical formulation. The loop continues until the model produces a final answer with no tool calls, OR a stopping condition fires (max iterations, cost budget, timeout).
Why loops need guardrails
Without guardrails, agents loop forever — calling the same tool with paraphrased arguments, burning budget, never converging. Stopping conditions (max_iter, cost budget, timeout) are mandatory in production. So is deduplication of tool calls and a 'reflect and decide' step.
The production agent loop
Task → LLM (with tools + max_iter + budget) → if tool_calls: dedupe against history, validate args, execute with timeout, append observation → if no tool_calls: return final answer → if max_iter or budget exceeded: return partial + escalate. Every step is traced.
Production agent loop
import json
from dataclasses import dataclass, field
@dataclass
class AgentConfig:
max_iterations: int = 10
max_cost_usd: float = 1.0
timeout_seconds: float = 60.0
@dataclass
class AgentState:
messages: list[dict] = field(default_factory=list)
iterations: int = 0
cost_usd: float = 0.0
tool_calls_made: list[str] = field(default_factory=list)
def run_agent(task: str, tools: dict, config: AgentConfig) -> str:
state = AgentState(messages=[{"role": "user", "content": task}])
while True:
# Stopping conditions
if state.iterations >= config.max_iterations:
return f"[Stopped at max_iterations={config.max_iterations}] " + force_answer(state)
if state.cost_usd >= config.max_cost_usd:
return f"[Stopped at cost budget] " + str(config.max_cost_usd) + " " + force_answer(state)
state.iterations += 1
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=state.messages,
tools=[t.definition() for t in tools.values()],
tool_choice="auto",
)
msg = response.choices[0].message
state.cost_usd += estimate_cost(response.usage)
# No tool calls → final answer
if not msg.tool_calls:
return msg.content
# Append the assistant message with tool calls
state.messages.append(msg.model_dump())
# Execute each tool call
for tc in msg.tool_calls:
tool = tools[tc.function.name]
args = json.loads(tc.function.arguments)
# Dedupe: skip if we called this exact tool+args recently
call_sig = f"{tc.function.name}:{json.dumps(args, sort_keys=True)}"
if call_sig in state.tool_calls_made[-3:]:
observation = '{"error": "Already called with these args. Try a different approach."}'
else:
state.tool_calls_made.append(call_sig)
observation = tool.execute(args)
state.messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": observation,
})
def force_answer(state: AgentState) -> str:
"""When budget exhausted, ask the model to answer with what it has."""
state.messages.append({
"role": "user",
"content": "Budget exhausted. Provide your best answer with current information.",
})
r = client.chat.completions.create(
model="gpt-4o-mini", messages=state.messages, tools=None
)
return r.choices[0].message.contentExperiment: stopping conditions
Toggle guardrails on/off. See how they prevent runaway agents.
What to observe
All three guardrails (max_iter, cost budget, dedup) are mandatory. Missing any one leads to runaway agents. The 'force_answer' fallback on budget exhaustion is critical — the agent must always return SOMETHING, not hang. Trace every step so you can debug.
Production agent loops
Production agents: max_iter + cost budget + timeout (all three), deduplication of recent tool calls, per-tool timeout, trace every step, force_answer on budget exhaustion, and a reflection step before the final answer ('Do I have enough to answer?'). For destructive tools: human approval gate.
Challenge
Your agent loops 25 times on a research task and costs $15. The system prompt says 'be thorough'. Diagnose and fix the loop. (See the 'Agent That Wouldn't Stop' challenge.)
Production checklist
Production checklist
0 of 9 checked
Knowledge check
An agent keeps calling search() with slightly different queries and never converges. Which fix is MOST effective?
Complete
You can now build a production agent loop with guardrails. Next: memory and planning — making agents persist context and decompose tasks.
Mark this chapter as complete
Track your progress and unlock the next chapter.