Command Palette

Search for a command to run...

Chapter 2·38 min read·Advanced

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

agent_loop.pypython
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.content

Experiment: stopping conditions

Toggle guardrails on/off. See how they prevent runaway agents.

Configure agent guardrails. Observe behaviour on a hard task.

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.

Continue learning