beginnercorereactreasoningactingpattern

ReAct: Reasoning-Action Pattern

The foundational Reason + Act pattern that powers every modern AI agent — from theory to production implementation.

ReAct: Reasoning-Action Pattern

Every AI coding assistant you use today — Claude Code, Cursor, Aider, SWE-Agent — is powered by the same fundamental loop: think, act, observe, repeat. This pattern has a name, a research pedigree, and a surprisingly simple formalization. It is called ReAct, and understanding it is the single most important prerequisite for understanding modern AI agents.

The Origin: Yao et al. 2022

ReAct was introduced by Shunyu Yao, Dian Yu, Jeffrey Zhao, Izhak Shafran, Thomas L. Griffiths, Yuan Cao, and Karthik Narasimhan in their 2022 paper "ReAct: Synergizing Reasoning and Acting in Language Models" (arXiv:2210.03629). The paper's key finding was deceptively simple:

Interleaving reasoning traces (Thought) with task-specific actions (Action) produces substantially better results than either reasoning alone or action alone.

Before ReAct, the field had two dominant paradigms:

  • Chain-of-Thought (CoT) prompting (Wei et al., 2022), where models produce step-by-step reasoning but never interact with the world.
  • Action-only agents, which could call tools or APIs but had no structured reasoning — they just reacted to inputs.

ReAct merged both: the model reasons about what to do, takes an action in the environment, observes the result, and reasons about what to do next. This creates a grounded, self-correcting loop that dramatically improves performance on tasks requiring both planning and external interaction.

The paper demonstrated ReAct on question answering, fact verification, and text-based game playing. On HotpotQA, ReAct improved the accuracy over Chain-of-Thought alone by roughly 10 absolute percentage points, and over action-only baselines by even more.

The Core Cycle

The ReAct pattern is a three-phase loop that repeats until the task is complete:

    ┌──────────────────────────────────────────────┐
    │              ReAct Cycle                       │
    │                                               │
    │   ┌──────────┐    ┌──────────┐    ┌────────┐ │
    │   │          │    │          │    │        │ │
    │   │  Thought │───>│  Action  │───>│Observa-│ │
    │   │          │    │          │    │  tion  │ │
    │   └────▲─────┘    └──────────┘    └───┬────┘ │
    │        │                               │      │
    │        └───────────────────────────────┘      │
    │                                               │
    └──────────────────────────────────────────────┘

Each phase has a specific role:

  • Thought: The model reasons about the current state, the goal, and what action to take next. This is explicit reasoning text — not hidden internal computation, but a legible chain of logic.
  • Action: The model performs a concrete operation: reading a file, running a command, executing a web search, editing code, calling an API.
  • Observation: The environment returns a result: file contents, command output, search results, test pass/fail. This grounds the model's next reasoning step in reality.

The cycle continues until the model emits a Finish action or reaches some terminal condition. This is fundamentally different from a single-shot prompt-and-response: ReAct agents can recover from errors, pursue multi-step plans, and adapt to unexpected observations.

A concrete trace from a ReAct agent answering a factual question:

Thought 1: I need to find the birth year of the author of "1984" to determine
           how old they were when they wrote it.
Action 1:  Search("author of 1984 novel")
Observation 1: George Orwell (Eric Arthur Blair), born 25 June 1903.
Thought 2: "1984" was published in 1949. Orwell was born in 1903.
           1949 - 1903 = 46 years old.
Action 2:  Finish(46)

Notice how the reasoning is grounded: the model does not guess the author's birth year. It searches for it, receives concrete evidence, and then reasons from that evidence.

How ReAct Differs from Pure Chain-of-Thought

The distinction between ReAct and Chain-of-Thought is critical and often misunderstood:

DimensionChain-of-Thought (CoT)ReAct
ReasoningInternal trace, no external groundingExplicit thoughts grounded in observations
World interactionNone — purely internal computationActions that affect external environment
Error correctionCannot verify its own reasoningObserves results and adjusts
Information accessLimited to training data + promptCan query tools, APIs, files at runtime
Hallucination riskHigh — no feedback loopLower — observations anchor reasoning
Context growthLinear with reasoning lengthGrows with actions + observations
Best forMath, logic, single-pass tasksMulti-step tasks requiring external data

Chain-of-Thought is powerful for mathematical reasoning and logical inference where all the information is present in the prompt. But when the agent needs to look things up, run code, or interact with a codebase, CoT alone falls short. It cannot verify that a function exists before suggesting to call it. It cannot check whether a test passes after proposing a fix.

ReAct's superpower is the observation step. Every time the agent acts, it receives fresh information from the environment. This information constrains and corrects its subsequent reasoning, making the agent far more reliable than a model that only thinks internally.

Implementation in Modern Tools

ReAct is not an abstract academic pattern — it is the concrete architecture underlying every production AI coding tool. Each tool implements the core Thought-Action-Observation cycle with domain-specific choices about what actions are available and how observations are formatted.

Claude Code: The Agent Loop IS a ReAct Loop

Claude Code, Anthropic's terminal-based coding agent, implements ReAct as its fundamental execution model. When you ask Claude Code to fix a bug, the loop looks like this:

Thought:  The error message mentions a missing import on line 12.
          I should read the file to understand the full context.
Action:   Read(file.py, lines 1-20)
Observation: <file contents showing no import statement>
Thought:  The file uses `requests.get()` but doesn't import requests.
          I need to add `import requests` at the top.
Action:   Edit(file.py, add_import)
Observation: <edit applied successfully>
Thought:  Let me verify the fix by running the tests.
Action:   Bash(python -m pytest test_file.py)
Observation: <test output: 12 passed, 0 failed>
Thought:  All tests pass. The fix is complete.
Action:   Finish

Claude Code's action set includes reading files, editing code, running shell commands, searching codebases, and invoking tools. The observation is always the raw output of the action — file contents, edit confirmations, command stdout/stderr. This fidelity is what makes the reasoning effective: the agent sees exactly what happened and can reason about it precisely.

Per the Claude Code documentation and design, the system is architected so that every user request triggers a ReAct loop. There is no separate "agent mode" — the ReAct pattern is the only mode.

Cursor: Zero-Entropy ReAct

Cursor, the AI-powered code editor, implements a ReAct loop with an important optimization. Cursor aims for what might be called zero-entropy ReAct: each action should produce the minimal necessary observation to inform the next reasoning step.

In practice, this means:

  • Tab-away operations: When Cursor suggests code, pressing Tab accepts it. The observation is just "accepted" — not a full diff. This keeps context pollution low.
  • Intent prediction: Cursor predicts what the user wants to do before they fully specify it. This means the Thought step is often partially pre-computed, reducing the reasoning burden.
  • Focused observations: Instead of returning entire file contents, Cursor's observation step returns only the relevant snippet — the lines around an edit, the specific error message, the relevant type information.

This is a practical necessity in an editor context where context windows are limited and must be shared with the user's own editing context. The alternative — feeding full observations back — leads to context explosion, which degrades reasoning quality over time.

The tradeoff is that zero-entropy ReAct is less robust when unexpected errors occur. If the filtered observation misses a critical detail, the agent's next reasoning step will be based on incomplete information. Cursor mitigates this by falling back to full observations when the agent explicitly requests more context.

Aider: Git-First ReAct

Aider (github.com/paul-gauthier/aider) implements ReAct with git as its primary state management and observation mechanism. The cycle is:

Thought:  The tests are failing because of a type mismatch in function signature.
Action:   Edit code (via LLM-generated diff)
Observation: Git diff shows what changed
Action:   Commit the change
Action:   Run tests
Observation: Test output (passed/failed, assertion details)
Thought:  Two tests still fail. The diff shows I changed the return type
          but didn't update the callers.
Action:   Read caller files

Aider's key architectural choice is that every edit is a git commit. This means observations are always grounded in version control: the agent can see diffs, can revert changes, and can reason about the full history of its actions. When Aider's "send to openreview" feature submits code for review, the entire ReAct trace — all thoughts, actions, and observations — becomes the commit message context.

This git-first approach makes Aider uniquely robust for long-running tasks. If the agent goes down a wrong path, the commit history provides a clean rollback mechanism that is absent in tools without persistent state.

LangChain / LangGraph: ReAct as a Framework Primitive

LangChain's AgentExecutor class is literally a ReAct loop implementation. Per the LangChain documentation, the agent executor:

  1. Passes the current state to the LLM.
  2. The LLM produces a Thought (reasoning text) and an Action (tool call).
  3. The executor runs the tool and captures the Observation.
  4. The executor appends the Observation to the state and repeats from step 1.
from langchain.agents import AgentExecutor, create_react_agent

# This is a ReAct loop configured with specific tools
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)

# Each .invoke() runs the full ReAct loop until Finish
result = agent_executor.invoke({"input": "Fix the failing test in test_auth.py"})

In LangGraph, ReAct is expressed as a state machine with explicit node transitions:

from langgraph.graph import StateGraph, END

graph = StateGraph(AgentState)
graph.add_node("agent", call_model)       # Thought
graph.add_node("tools", tool_executor)     # Action
graph.add_node("observe", process_result) # Observation

graph.add_edge("agent", "tools")
graph.add_edge("tools", "observe")
graph.add_conditional_edges("observe", should_continue, {
    "continue": "agent",
    "end": END
})

LangGraph's advantage is that the ReAct loop becomes composable: you can insert checkpoints, human review steps, parallel tool execution, or error recovery nodes without restructuring the fundamental cycle.

From ReAct to the Agent Loop

The simple three-step ReAct cycle is the seed from which the full agent loop grows. In production systems, the basic pattern gets extended with several additional capabilities:

    ┌─────────────────────────────────────────────────────────┐
    │                Full Agent Loop                          │
    │                                                         │
    │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────┐ │
    │  │ Plan     │  │ Thought  │  │ Action   │  │Observa-│ │
    │  │ (Goal    │─>│ (Reason  │─>│ (Execute)│─>│  tion  │ │
    │  │  decom-  │  │  about   │  │          │  │        │ │
    │  │  pose)   │  │  action) │  │          │  │        │ │
    │  └──────────┘  └──────────┘  └──────────┘  └───┬────┘ │
    │       ▲                                       │      │
    │       │    ┌──────────┐  ┌──────────┐         │      │
    │       └────│ Verify   │<─│ Reflect  │<────────┘      │
    │            │ (Check   │  │ (Error   │                │
    │            │  result)  │  │  recover)│                │
    │            └────┬─────┘  └──────────┘                │
    │                 │                                     │
    │                 v                                     │
    │            ┌──────────┐                             │
    │            │ State    │                              │
    │            │ Manager  │                              │
    │            └──────────┘                              │
    └─────────────────────────────────────────────────────────┘

The extensions beyond basic ReAct include:

State management: Production agents maintain structured state — files modified, tests passed/failed, errors encountered. This state persists across iterations and informs the Thought step. Aider uses git for this. Claude Code maintains an internal workspace state. SWE-Agent uses a structured state object passed through each iteration.

Verification: After an action, the agent doesn't just observe — it verifies. "I edited the file. Did the tests pass? Did I introduce new warnings? Does the diff look correct?" Verification adds a correctness check that pure ReAct lacks.

Reflection: When an observation indicates failure (tests fail, compilation error, user rejection), the agent reflects on what went wrong. This is a specialized Thought step that analyzes the root cause rather than planning the next action.

Error recovery: If an action fails (command not found, file doesn't exist, API timeout), the agent needs a strategy for recovery — retry with different arguments, try an alternative approach, or escalate to the user.

Termination: Pure ReAct terminates when the model says "Finish." Production systems add safeguards: maximum iteration counts, timeout timers, and cost budgets to prevent runaway loops.

These extensions are discussed in depth in the Agent Loop and Closed Loop Iteration articles.

Minimal ReAct Implementation

Here is a minimal but complete ReAct implementation in Python that demonstrates the core pattern without any framework overhead:

import re
from typing import Callable, Dict

def react_loop(
    task: str,
    tools: Dict[str, Callable],
    llm: Callable,
    max_iterations: int = 10
) -> str:
    """Execute a ReAct loop: Thought -> Action -> Observation -> repeat."""
    
    context = f"Task: {task}\n"
    
    for i in range(max_iterations):
        # --- Thought: LLM reasons about current state ---
        prompt = (
            f"{context}\n"
            f"Thought {i + 1}: "
        )
        thought = llm(prompt)
        context += f"Thought {i + 1}: {thought}\n"
        
        # --- Action: Parse tool call from LLM output ---
        action_match = re.search(r'Action: (\w+)\((.+?)\)', thought)
        if not action_match:
            # Check for Finish action
            finish_match = re.search(r'Finish\((.+?)\)', thought)
            if finish_match:
                return finish_match.group(1)
            context += f"Error: No valid action found. Try again.\n"
            continue
        
        tool_name, tool_input = action_match.group(1), action_match.group(2)
        
        if tool_name == "Finish":
            return tool_input
        
        # --- Observation: Execute the tool and capture result ---
        if tool_name not in tools:
            observation = f"Error: Unknown tool '{tool_name}'."
        else:
            try:
                observation = str(tools[tool_name](tool_input))
            except Exception as e:
                observation = f"Error: {e}"
        
        context += f"Action {i + 1}: {tool_name}({tool_input})\n"
        context += f"Observation {i + 1}: {observation}\n"
    
    return "Error: Maximum iterations reached without solution."

# --- Example usage with file-editing tools ---

def read_file(path: str) -> str:
    with open(path) as f:
        return f.read()[:500]  # Truncate for brevity

def run_command(cmd: str) -> str:
    import subprocess
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.stdout + result.stderr

tools = {
    "read_file": read_file,
    "run_command": run_command,
}

# result = react_loop("Fix the failing test in test_auth.py", tools, my_llm)

This implementation, while minimal, contains every essential element: explicit reasoning traces, tool dispatch, observation capture, and termination logic. a 30-line ReAct implementation follows the same structure, confirming that the core idea is inherently simple.

The Zero-Entropy Variant

Cursor's approach highlights an important design dimension: observation fidelity. In a standard ReAct implementation, observations are raw and complete — full file contents, full command output, full API responses. This is maximally informative but comes at a cost.

Every observation consumes context window space. In a long-running task with many iterations, observations accumulate. The LLM's reasoning quality degrades as the context fills with noise — old observations that are no longer relevant, verbose logs that contain only one useful line, repeated error messages.

The zero-entropy variant addresses this by filtering and compressing observations:

Standard ReAct Observation:
  $ python -m pytest tests/
  ======================== test session starts ========================
  platform linux -- Python 3.11.4, pytest-7.4.0
  rootdir: /home/user/project
  collected 47 items

  tests/test_auth.py::test_login PASSED                        [  2%]
  tests/test_auth.py::test_logout PASSED                        [  4%]
  tests/test_auth.py::test_session_expiry FAILED               [  6%]
  tests/test_auth.py::test_token_refresh PASSED                [  8%]
  ...
  tests/test_utils.py::test_parse_config PASSED                [95%]
  ========== 46 passed, 1 failed in 3.42s ==========

Zero-Entropy Observation:
  46 passed, 1 failed:
    FAILED tests/test_auth.py::test_session_expiry
    AssertionError: Expected session to expire after 3600s, got None

The zero-entropy version contains exactly the information needed for the next Thought step: which test failed and why. The 45 passing tests are irrelevant noise. By filtering them out, the context stays clean and the agent's reasoning stays focused.

However, as noted in Cursor's design tradeoffs, aggressive filtering risks hiding critical information. A passing test might have produced a warning that is relevant to the next step. A failed test's output might contain a stack trace that points to a different root cause than the assertion message suggests. The optimal observation fidelity depends on the task and the agent's sophistication.

Common Failure Modes

ReAct loops are powerful but fragile. Understanding their failure modes is essential for building robust agent systems.

Reasoning Loops: Thinking Forever Without Acting

Thought 1: I need to understand the codebase structure first.
Thought 2: But I should also consider the test requirements.
Thought 3: Let me think about the architecture before reading any files.
Thought 4: Actually, maybe I should plan my approach more carefully.
...

The agent generates increasingly elaborate reasoning without ever taking an action. This happens when the LLM is too cautious, when the prompt doesn't encourage action-taking, or when the model has been fine-tuned to avoid tool calls. The fix is structural: enforce a maximum number of Thoughts between Actions, or restructure the prompt to make Actions the expected output.

Action Loops: Acting Forever Without Reasoning

Action 1:   Edit file A
Observation 1: Edit applied
Action 2:   Edit file B
Observation 2: Edit applied
Action 3:   Edit file A again (reverting Action 1)
Observation 3: Edit applied
Action 4:   Edit file B again (reverting Action 2)
...

The agent takes actions without genuine reasoning, often repeating or contradicting previous actions. This occurs when observations don't provide enough signal for meaningful reflection, or when the LLM's action bias is too high. Fixes include requiring explicit reasoning traces between actions (the "Thought" step is not optional), and implementing idempotency checks that detect when the agent is revisiting the same state.

Context Explosion

Iteration 1:  context = 2,000 tokens
Iteration 5:  context = 18,000 tokens
Iteration 10: context = 45,000 tokens
Iteration 15: context = 89,000 tokens
Iteration 20: context = 180,000 tokens (model quality degrades)

Every iteration adds a Thought, Action, and Observation to the context. Without management, the context grows linearly with iteration count. Eventually the context window fills, earlier observations are truncated, and the agent loses access to the very information it needs to reason effectively. Mitigation strategies include:

  • Sliding window: Keep only the last N observations in context.
  • Summarization: Periodically compress the observation history into a summary.
  • Selective retention: Keep observations that are marked as important by the agent's own reasoning.
  • State extraction: Maintain a separate structured state object that captures the essential information from all observations, independent of the context history.

Aider's git-based approach is a natural defense against context explosion: the git diff is a compressed representation of all changes, and the commit history provides a retrievable record without keeping every observation in the active context.

Misaligned Actions

The agent selects a valid tool but with incorrect arguments, or picks the wrong tool for the task. This is the ReAct equivalent of "the LLM hallucinated." The observation step provides some correction — the tool returns an error — but if the agent doesn't understand why the action failed, it may retry with similarly wrong arguments. Tool descriptions, usage examples, and validation in the tool layer all help reduce this failure mode.

Why ReAct Matters for Loop Engineering

ReAct is the atomic unit of loop engineering. Every loop in every AI system — from a simple CLI agent to a multi-agent orchestration framework — is built on this foundation. The Thought-Action-Observation cycle is the simplest possible loop that combines reasoning with interaction, and every more complex loop is an extension of it.

When we talk about "closing the loop," we mean ensuring that observations feed back into reasoning. When we talk about "loop quality," we mean the fidelity of each Thought, Action, and Observation step. When we talk about "loop failures," we mean the failure modes described above.

Understanding ReAct at a visceral level — having built one, debugged one, and observed its failure modes — is the foundation for understanding every other pattern in loop engineering. The Agent Loop article builds on this foundation by adding verification, state management, and production hardening. The Task Loop article extends it further with goal decomposition and multi-step planning.

Key References