The Agent Loop: ReAct and Its Descendants
How ReAct agents execute tools, process observations, enforce stop conditions, and compare with plan-and-execute.
A tool-using agent repeatedly asks a model for the next action, executes that action, and returns the observation. The runtime must decide when to stop, validate tool arguments, enforce budgets, and preserve enough state for the next iteration. Without those controls, a capable model can still enter an expensive loop that never produces a final answer.
The agent control loop
Simon Willison uses a practical definition: “An LLM agent runs tools in a loop to achieve a goal.” It identifies the model, tool interface, and iterative control flow.
- LLM: The decision-maker. Picks the next tool call, interprets the result, decides when to stop.
- Tools in a loop: Not one call, not one tool; repeated invocations where each result feeds the next decision. This is the part that separates an agent from a glorified function call.
- To achieve a goal: There’s a terminal condition. Without it you have an infinite loop, not an agent.
Lilian Weng’s earlier framing; “Agent = LLM + memory + planning skills + tool use” (June 2023); adds the two ingredients that the loop sits on top of: memory (the conversation history is the cheapest, and the only one most agents need) and planning (which may be implicit in the loop or explicit as a separate step, see below).
Frame it operationally and the loop is a control structure with five moving parts: the driver (your code) calls the model with the context (prompt + history + tool schemas); the model emits tool calls that the driver executes against a tool surface; results splice back into context; loop. Every word in that sentence has its own engineering decisions. Most production failures are in the driver, not the model.
ReAct: the canonical loop
The pattern that nailed the loop down for LLMs is ReAct (Yao et al., 2022); Reasoning + Acting. The model alternates three step types:
- Thought: Free-text reasoning about what to do next. The model talks to itself.
- Action: A tool invocation with typed arguments; the structured request covered in the tool-use article.
- Observation: The tool result, fed back into the context for the next thought.
ReAct’s original framing was prompt-only; you few-shot the model with examples of Thought:/Action:/Observation: triples and it learns the rhythm. Modern LLMs internalize this without examples; the prompt-only framing has been absorbed into the post-training data of every frontier model. What ReAct contributed wasn’t a prompt template; it was the insight that explicit reasoning steps between actions sharply improve tool-using accuracy. On HotpotQA the original paper saw the model overcome hallucination by reasoning aloud about what it had observed and what it still needed; on ALFWorld and WebShop the gains over imitation learning were 34 and 10 absolute percentage points respectively.
The mechanical artifact of ReAct in 2026 is the structured assistant turn: a text block containing the thought, followed by one or more tool_use blocks containing the action. Anthropic’s tool-use API bakes this in; the response with stop_reason: "tool_use" is exactly one ReAct iteration. Set extended_thinking and the thought becomes a separate thinking block; the structure is the same. OpenAI’s tool calls have the same shape with different field names.
Execute one iteration
Walk through a single ReAct iteration end to end. The driver holds three pieces of state: a messages list (the conversation), a tools array (the schemas), and a step counter.
- Call the model: The driver POSTs
messagesandtoolsto the provider. With prompt caching configured on the tool block and system prompt, this call is cheap; without it, you re-prefill the tools on every step. - Receive an assistant turn: The response is a list of content blocks. The driver appends the entire content array back to
messages; the API expects byte-identical blocks on the next call. This is the part most home-grown loops botch: stripping the thought blocks and keeping only the tool calls breaks future-turn coherence on every frontier model. - Inspect
stop_reason: If it isn’t"tool_use"(or the OpenAI equivalent"tool_calls"), the loop is done; return the assistant text. - For each
tool_useblock, dispatch: Look up the tool, validate args (or trust strict-mode constraints), execute. Capture the result, the success/error flag, and the duration. - Append a user turn of
tool_resultblocks: Each must carry the matchingtool_use_id. Errors go in the same shape withis_error: true; the model recovers cheaply from a typed error message; raising a Python exception out of the loop is almost never what you want. - Increment the step counter: Compare against the cap. If exceeded, abort with whatever partial progress you have.
That’s it. Every production agent loop in existence is some elaboration of this six-step body. The elaborations are where the interesting decisions live.
Enforce stopping conditions
The runtime decides when to stop. A model may continue requesting tools because each observation suggests another possible check. The driver enforces the limit independently of the prompt.
A defensible stopping condition is a disjunction of cheap predicates evaluated after each step:
stop_reason != "tool_use": The natural exit; the model didn’t ask for another tool. This is the only stopping condition the model fires; everything else is the driver.- Step cap: Hard maximum number of iterations, no negotiation. The Vercel AI SDK’s default is
stepCountIs(20); most production agents tune this between 5 and 50 depending on task variance. - Wall-clock deadline: Total time across the loop. Critical for user-facing chat where p99 latency matters; less critical for batch agents.
- Token budget: Sum of input + output tokens across all calls. The cheapest predicate to evaluate, the easiest to forget.
- Dollar budget: Same shape as tokens, denominated in money. Worth maintaining as a separate counter because the dollar/token ratio differs across cached vs uncached input and across models.
- No-progress detection: Did the last N steps make distinguishable changes to state? Two identical tool calls in a row is a strong signal the loop is stuck; three is decisive. The simplest implementation is hashing the
(tool_name, args)tuple and looking for repeats. - Goal predicate: “Has the model called the
submit_final_answertool?”; a tool whose only purpose is to terminate the loop. Vercel’shasToolCall(name)is exactly this pattern.
The disjunction matters. Step cap alone is a hammer that takes effective work and partial results with it; a goal predicate alone never fires for a confused model that hallucinates the wrong-named tool. Compose them: stop on the first of (stop_reason, step cap, deadline, budget, no-progress, goal). The fork-bomb parallel from operating systems is exact; every kernel needs an OOM killer, every agent needs an enforced budget. The agent-budgets-and-runaway-prevention article is the dedicated walk-through: the seven primitives every budget needs (step cap, deadline, token ceiling, dollar cap, per-tool quota, no-progress streak, oscillation detection, external abort), the OS heritage of each one, and the runnable harness implementations.
Plan-and-execute: the batching alternative
ReAct normally uses one model call per tool decision. Twelve tool invocations can therefore require twelve model round trips, repeated prefix processing, and repeated network latency. Predictable tasks may be cheaper with a plan-and-execute design. Planning Agents vs Reactive Agents compares the cost of replanning with ReAct, ReWOO, and search-based variants.
Plan-and-execute inverts the loop. First call: ask the model to produce a step-by-step plan as a structured object (a JSON list of typed steps). Then execute the steps sequentially; either with smaller, cheaper executor calls or with raw code, depending on how rigidly the plan can be specified. Optionally re-plan after each step or on failure. LangChain’s plan-and-execute writeup is the canonical reference; the original research lineage runs through Plan-and-Solve (Wang et al., ACL 2023).
The advantages and disadvantages are mirror-image to ReAct’s:
| ReAct | Plan-and-execute | |
|---|---|---|
| LLM calls per task | One per step | One plan + one per step (executor can be smaller) |
| Adaptability | High; every step replanned implicitly | Low; plan is fixed until re-planning |
| Quality on well-specified tasks | Variable; model can drift mid-task | Better; plan forces whole-task reasoning |
| Quality on ill-specified tasks | Better; reacts to surprises | Worse; plan may be wrong; replanning is expensive |
| Latency | Higher; serial round-trips | Lower; executor can use smaller model |
| Debuggability | Hard; reasoning is interleaved | Easier; plan is a typed artifact |
| Token cost | High; model + tools re-prefilled | Lower; executor sees less |
A hybrid can use ReAct while the path is uncertain, switch to a plan when the remaining sequence is predictable, and return to ReAct after an unexpected result. Reflexion adds a written reflection after a failed attempt without updating model weights. Tree of Thoughts explores and scores several candidate paths when branching is worth the extra calls.
Code: a ReAct loop in Python with the Anthropic SDK
A research-assistant agent with three tools: web search, document fetch, and final answer. Install: pip install anthropic. Uses the Anthropic SDK.
| |
the budgets are checked before the API call; checking after the fact is how you accidentally double your spend right at the limit. The no-progress detector is dumb but effective: a hash of the last three (tool_name, args) tuples. If they’re identical, the model is stuck in a loop and no amount of additional steps will help. submit_final_answer is the only “happy path” exit other than the model declining to call a tool; having an explicit terminator tool is much easier to test and reason about than relying on the model to know when to stop emitting tool calls.
The step cap (max_steps=12) is the floor of the abstraction. Without it, a confused model with access to search_web will happily search forever for any sufficiently vague question. The cap is the fork-bomb backstop; the deadline and the no-progress detector are finer-grained nets above it.
What the harness owns
A useful frame: the agent loop has two layers, and the harness; the runtime that drives the model; owns the bottom layer entirely.
- Top layer (model decisions): which tool, with what arguments, when to stop emitting tool calls. The model’s job.
- Bottom layer (harness duties): budget enforcement, retry policy, observability, error transformation, prompt-cache hygiene, message-list construction, streaming, cancellation. Always the driver’s job.
A prompt cannot enforce a token budget or deterministically detect repeated calls. The harness can count usage, compare call signatures, and stop execution. It also owns context assembly, tool dispatch, streaming, prompt-cache management, error recovery, cost accounting, and telemetry. See Agent Harness Anatomy and Building effective agents.
Further reading
- Simon Willison; “An LLM agent runs tools in a loop to achieve a goal”; the definition-fixing post that finally pinned down what “agent” means in the post-2024 sense. Short, sharp, and the working vocabulary the rest of the field has converged on.
- Anthropic; “Building effective agents”; the December 2024 engineering writeup that distinguishes workflows from agents and walks through the prompt-chaining, routing, parallelization, orchestrator-workers, and evaluator-optimizer patterns. Read this before reaching for a framework.
- Lilian Weng; “LLM Powered Autonomous Agents”; the 2023 survey that fixed the planning/memory/tools decomposition and is still the cleanest concept map for the agent surface.
- LangChain; “Plan-and-Execute Agents”; the canonical writeup on the planner/executor split, with the LangGraph implementation reference. Read alongside the Plan-and-Solve paper (Wang et al., ACL 2023) for the research lineage.