Skip to content

Chapter 4: The ReAct Pattern - Reason + Act

Theoretical Foundations

In the previous chapters, we explored the foundational building blocks of autonomous agents: tools, prompts, and the basic agent loop. We established that an agent, at its core, is a system that uses an LLM to decide what to do next, often by calling external tools. However, the initial implementations we discussed were largely linear. The agent would receive a query, reason about it once, execute a tool, and then produce a final answer. This is effective for simple, single-step tasks, but it breaks down when faced with complex, multi-step problems that require iterative planning, course correction, and the synthesis of information from multiple sources.

The ReAct (Reason + Act) pattern is the architectural solution to this limitation. It transforms the agent from a simple, one-shot decision-maker into a persistent, reflective process. The core idea is to create a cyclical loop where the agent continuously alternates between internal reasoning and external action. This pattern is not just a technical implementation; it's a paradigm shift that mirrors human problem-solving. When faced with a difficult question, you don't just blurt out an answer. You think, you plan, you might look something up, you process the new information, and then you think again. The ReAct pattern gives this same cognitive capability to our agents.

The Anatomy of a ReAct Loop: Thought, Action, Observation

The ReAct loop is built upon a simple but powerful triad of states that the agent cycles through. This is the "Reason + Act" in its most granular form.

  1. Thought (Reasoning): This is the agent's internal monologue. It's the LLM's opportunity to analyze the current state of the problem, review the history of previous actions and their results (observations), and formulate a plan for the next step. The agent might reason:

    • "The user asked for the current stock price of Apple. I don't know this off the top of my head, so I need to find a reliable data source."
    • "I previously called the get_stock_price tool for Apple, but the result was an error. The error message said 'Invalid ticker symbol'. I need to double-check the correct ticker symbol. It's likely 'AAPL'."
    • "I have the current price, but the user also asked for a 52-week high. I need to make another tool call to get that information before I can synthesize a complete answer."

    The "Thought" is the crucial reasoning step that precedes any action. It's the agent's way of saying, "Here's what I know, here's what I need, and here's my plan to get it."

  2. Action (Execution): Based on its internal reasoning, the agent decides to perform an action. This is almost always a tool call. The agent selects a specific tool from its available toolkit and provides the necessary arguments. This is the "hands-on" part of the loop where the agent interacts with the outside world. For example:

    • Action: Call tool 'get_stock_price' with argument { ticker: 'AAPL' }
    • Action: Call tool 'search_web' with argument { query: 'current CEO of OpenAI' }

    The action is a direct consequence of the thought. Without the reasoning step, the action would be blind and arbitrary.

  3. Observation (Feedback): After an action is executed, the external world provides feedback. This feedback is the result of the tool call, packaged as an "Observation." This observation is then fed back into the agent's context for the next reasoning cycle. The observation could be:

    • The successful output of a tool call (e.g., { "price": 185.30 }).
    • An error message from a failed API call (e.g., Error: Rate limit exceeded).
    • The content retrieved from a web search.

    The observation is the agent's new piece of information. It's the data point that informs the next "Thought" and allows the agent to adapt its strategy.

These three steps—Thought, Action, Observation—form the fundamental unit of the ReAct loop. The agent repeats this cycle until it has gathered enough information and reasoned sufficiently to confidently provide a final answer to the user.

The Cyclical Graph Structure: Implementing the Loop in LangGraph

In LangGraph.js, this cyclical pattern is not implemented with a while loop in a single function. Instead, we model it as a cyclical graph structure. This is a critical architectural decision that provides clarity, state management, and resilience.

Let's break down how this graph is constructed. We have nodes (our agents, tools, or decision points) and edges (the transitions between them).

  • Nodes:

    • agent: This node contains the core LLM call. It receives the current state (including the conversation history and any observations) and is prompted to generate the next step in the ReAct format (i.e., a Thought and an Action).
    • tools: This is not a single node, but a collection of nodes, one for each tool the agent can use. When the agent node decides to call a tool, the graph routes to the corresponding tool node.
    • should_continue: This is a special decision node. After the agent node runs, this node inspects the output. Is the agent's response a final answer, or is it another tool call? This node's sole purpose is to determine the next edge to follow, acting as the traffic controller for our graph.
  • Edges:

    • agent -> should_continue: After the agent reasons and proposes an action, the graph moves to the decision node.
    • should_continue -> tools: If the decision is "continue" (i.e., the agent wants to call a tool), the graph routes to the appropriate tool node.
    • tools -> agent: This is the critical cyclical edge. After a tool executes and its result (the Observation) is added to the state, the graph routes back to the agent node. This completes the loop, giving the agent the new information it needs to reason again.
    • should_continue -> END: If the decision is "end" (i.e., the agent has produced a final answer), the graph routes to a terminal state, and the process stops.

This graph-based approach makes the agent's workflow explicit and auditable. You can visualize the exact path the agent took to arrive at an answer.

This diagram illustrates the agent’s graph-based workflow, visually mapping the explicit, auditable path taken to arrive at a final answer.
Hold "Ctrl" to enable pan & zoom

This diagram illustrates the agent’s graph-based workflow, visually mapping the explicit, auditable path taken to arrive at a final answer.

Analogy: The Assembly Line vs. The Expert Workshop

To understand the power of the cyclical ReAct pattern, let's use an analogy comparing a simple linear agent to a ReAct agent.

The Linear Agent as an Assembly Line Robot: Imagine a simple industrial robot on an assembly line. It's programmed to perform one specific task: "Pick up part A and screw it into slot B." It does this perfectly, but it has no awareness of the overall product. If part A is missing, the robot will either freeze or try to screw nothing into slot B, causing a failure. It cannot adapt. It follows a single, pre-defined path from start to finish. This is analogous to a simple agent that runs a single LLM call, tries one tool, and then stops. It's efficient for simple, predictable tasks but brittle in the face of complexity or unexpected conditions.

The ReAct Agent as an Expert Workshop: Now, imagine an expert artisan in a workshop. They are given a complex request: "Build a custom wooden chair." The artisan doesn't just start cutting wood. They engage in a cyclical process: 1. Thought (Plan): "I need to design the chair, select the right wood, cut the pieces, assemble them, and finish the surface." 2. Action (Execute): They go to their toolbench and select a saw to cut a piece of wood. 3. Observation (Feedback): They inspect the cut piece. Is it the right length? Is the cut clean? If not, they adjust their technique. 4. Thought (Adapt): "The first leg is cut. Now I need to cut three more, ensuring they are all identical." 5. Action (Execute): They use the saw again for the second leg. 6. Observation (Feedback): "This leg is slightly off. I need to check my saw's angle."

This iterative, reflective process continues until the chair is built to the artisan's satisfaction. The ReAct agent is this expert workshop. It doesn't just perform a single action; it reasons, acts, observes the result, and uses that new information to inform its next move. This cyclical process allows it to handle ambiguity, recover from errors (like a failed API call), and synthesize information from multiple steps to achieve a complex goal.

The "Why": Handling Complexity and Ambiguity

The primary reason for adopting the ReAct pattern is to solve problems that are inherently ambiguous or require multi-step information gathering. Consider the user query: "What are the key differences between the latest MacBook Pro and the previous generation, and which one would you recommend for a video editor on a budget?"

A linear agent would fail here. It might try to call a single tool to get "MacBook Pro differences," but the information is too broad and nuanced. A ReAct agent, however, can break this down:

  1. Thought: "This is a two-part question. First, I need to find the specs for the latest MacBook Pro and the previous generation. Second, I need to evaluate them based on the needs of a video editor with a budget constraint. Let's start by finding the specs."
  2. Action: search_web for "latest MacBook Pro specs 2024".
  3. Observation: (Receives detailed specs for the M3 Max chip, new display, etc.)
  4. Thought: "Okay, I have the specs for the new model. Now I need the specs for the previous generation (M2 Pro/Max)."
  5. Action: search_web for "MacBook Pro M2 specs".
  6. Observation: (Receives specs for the M2 model).
  7. Thought: "Now I have both sets of specs. The key difference is the M3 chip's performance and the new display. For a video editor, the CPU/GPU performance is critical. The M3 offers significant gains, but it's also more expensive. The budget constraint is key. The M2 model is now cheaper and still extremely powerful for video editing. I can now formulate a recommendation."
  8. Action: (The agent decides it has enough information and generates a final answer).

This step-by-step process, enabled by the cyclical graph, allows the agent to manage complexity by breaking it into manageable, sequential steps. The state of the graph holds the context of what has been done, preventing redundant work and enabling informed decision-making.

Under the Hood: State Management and Termination

The effectiveness of a ReAct loop hinges on two critical components: robust state management and clear termination conditions.

State Management: The state in a LangGraph is the single source of truth for the entire loop. It's an object that persists across every cycle of Thought, Action, and Observation. A typical state for a ReAct agent would look something like this:

// A conceptual representation of the agent's state
type AgentState = {
  // The original user query
  input: string;

  // The full conversation history, formatted for the LLM
  // This is where the agent's "Thoughts" and "Observations" are stored
  messages: Array<{
    role: 'user' | 'assistant' | 'tool';
    content: string;
  }>;

  // A flag to control the graph's flow
  shouldContinue: 'continue' | 'end';

  // Any other intermediate data the agent needs to track
  // For example, a counter for loop iterations to prevent infinite loops
  iterations: number;
};

When the agent node runs, it receives this entire state. It appends its new "Thought" and "Action" to the messages array. When a tool node runs, it appends the "Observation" to the messages array. This growing history is then fed back into the agent node in the next cycle, giving it the full context of its previous reasoning and actions. This is fundamentally different from a stateless API call where each request is independent. The ReAct loop is stateful and contextual.

Termination Conditions: A cyclical loop that never ends is a bug. The should_continue node is our safeguard. It uses logic to decide when the loop is complete. Common termination strategies include:

  • Final Answer Detection: The most common method. The agent is prompted to structure its final output in a specific format (e.g., Final Answer: ...). The should_continue node simply checks if the agent's latest response contains this marker. If it does, the graph terminates. If not, it assumes the agent is still in a "Thought/Action" cycle and continues.
  • Maximum Iterations: To prevent infinite loops (e.g., the agent gets stuck in a reasoning error), we can add a counter to the state (iterations). The should_continue node checks if iterations has exceeded a predefined limit (e.g., 10). If so, it terminates the loop, even if a final answer hasn't been found, and can return an error message or the partial context.
  • Tool-Specific Triggers: A tool itself can signal that the process is complete. For example, a "code execution" tool might return a result that indicates the task is finished, and the should_continue node can be configured to terminate based on this specific output.

By making the cyclical structure explicit in a graph and managing state and termination carefully, we move from brittle, one-shot agents to robust, resilient systems capable of tackling real-world complexity. The ReAct pattern is the engine that drives this capability, turning the simple concept of "reasoning and acting" into a powerful, scalable architecture.

Basic Code Example

The ReAct (Reason + Act) pattern is the foundational loop for autonomous agents. It mimics human problem-solving: we reason about the current situation, decide on an action, execute it, observe the result, and then repeat the cycle. In a SaaS application, this enables an agent to interact with external tools (like a database, an API, or a search engine) to fulfill a user's request dynamically.

We will build a "Customer Support Agent" for a SaaS dashboard. This agent can query a user's subscription status (acting as a tool) and reason about the next steps based on the data retrieved.

Visualizing the ReAct Loop

The flow of logic in a ReAct agent is cyclical until a condition is met (like finding the answer).

The ReAct agent operates in a cyclical loop, repeatedly querying a tool (like a user's subscription status) and reasoning about the retrieved data to determine the next steps until the final answer is reached.
Hold "Ctrl" to enable pan & zoom

The ReAct agent operates in a cyclical loop, repeatedly querying a tool (like a user's subscription status) and reasoning about the retrieved data to determine the next steps until the final answer is reached.

Self-Contained TypeScript Example

This example simulates a backend API route (like Next.js API routes) using pure TypeScript. It uses a mock database and a mock LLM to demonstrate the ReAct loop without external dependencies.

// ==========================================
// 1. TYPE DEFINITIONS & INTERFACES
// ==========================================

/**
 * Represents the state of our agent at any point in the loop.
 * This is the "Memory" of the agent.
 */
interface AgentState {
    input: string;
    conversationHistory: string[];
    context: Record<string, any>; // Stores retrieved data (e.g., user subscription)
    shouldContinue: boolean;
}

/**
 * Defines the structure of a tool the agent can use.
 */
interface Tool {
    name: string;
    description: string;
    // The actual function to execute
    execute: (args: any) => Promise<any>;
}

// ==========================================
// 2. MOCK TOOLS (The "Act" Phase)
// ==========================================

/**
 * Simulates an internal SaaS API to check user subscription status.
 * In a real app, this would connect to a database (Postgres, MongoDB).
 */
const checkSubscriptionTool: Tool = {
    name: "check_subscription",
    description: "Use this to check the user's current subscription plan and status.",
    execute: async (args: { userId: string }) => {
        console.log(`[Tool Execution] Checking subscription for user: ${args.userId}...`);

        // Simulate database latency
        await new Promise(resolve => setTimeout(resolve, 100));

        // Mock Data
        if (args.userId === "user_123") {
            return { plan: "Pro", status: "Active", expiresAt: "2024-12-31" };
        }
        return { plan: "Free", status: "Expired", expiresAt: "2023-01-01" };
    }
};

const tools = [checkSubscriptionTool];

// ==========================================
// 3. MOCK LLM (The "Reason" Phase)
// ==========================================

/**
 * Simulates an LLM (like GPT-4) that decides which tool to use.
 * In production, this calls an actual LLM API with a structured output schema.
 * 
 * @returns A string representing the LLM's decision: "TOOL:tool_name:args" or "FINAL:answer".
 */
async function mockLLMReasoning(state: AgentState): Promise<string> {
    const lastMessage = state.conversationHistory[state.conversationHistory.length - 1];
    const contextSummary = JSON.stringify(state.context);

    console.log("\n[LLM Reasoning] Analyzing state...");

    // Simple heuristic to simulate LLM logic for this demo
    // Real implementation would use a prompt like:
    // "Given history: X and context: Y, decide the next step."

    if (Object.keys(state.context).length === 0) {
        // We have no info yet. Let's ask the tool for data.
        console.log("[LLM Reasoning] No context found. Deciding to call 'check_subscription'.");
        return `TOOL:check_subscription:{"userId": "user_123"}`;
    } 

    // We have info. Let's formulate an answer.
    if (state.context.plan === "Pro") {
        console.log("[LLM Reasoning] User is Pro. Formulating final answer.");
        return `FINAL:The user has an active Pro subscription valid until ${state.context.expiresAt}.`;
    } else {
        console.log("[LLM Reasoning] User is not Pro. Formulating final answer.");
        return `FINAL:The user's subscription is ${state.context.status}. Please upgrade to Pro.`;
    }
}

// ==========================================
// 4. THE REACT AGENT (The Orchestrator)
// ==========================================

/**
 * The core ReAct loop implementation.
 * 
 * 1. Initialize State
 * 2. Loop:
 *    a. Reason (LLM decides action)
 *    b. Act (Execute Tool)
 *    c. Observe (Update State)
 * 3. Terminate when final answer is ready.
 */
class ReActAgent {
    private state: AgentState;

    constructor(initialInput: string) {
        this.state = {
            input: initialInput,
            conversationHistory: [initialInput],
            context: {},
            shouldContinue: true
        };
    }

    /**
     * Executes the ReAct loop.
     */
    async run(): Promise<string> {
        console.log("🚀 Starting ReAct Agent Loop...");

        // Loop guard to prevent infinite execution (safety mechanism)
        let steps = 0;
        const MAX_STEPS = 5;

        while (this.state.shouldContinue && steps < MAX_STEPS) {
            steps++;
            console.log(`\n--- Step ${steps} ---`);

            // 1. REASON: Ask LLM what to do
            const llmDecision = await mockLLMReasoning(this.state);

            // 2. ACT & OBSERVE: Parse decision and execute
            if (llmDecision.startsWith("TOOL:")) {
                // Parse: "TOOL:check_subscription:{\"userId\":\"user_123\"}"
                const [_, toolName, argsStr] = llmDecision.split(":");

                const tool = tools.find(t => t.name === toolName);

                if (tool) {
                    const args = JSON.parse(argsStr);
                    const result = await tool.execute(args);

                    // Update State with Observation
                    this.state.context = result;
                    this.state.conversationHistory.push(`Tool Result: ${JSON.stringify(result)}`);
                } else {
                    throw new Error(`Tool ${toolName} not found.`);
                }
            } 
            else if (llmDecision.startsWith("FINAL:")) {
                // Parse: "FINAL:The user has..."
                const answer = llmDecision.split(":")[1];

                this.state.conversationHistory.push(`Final Answer: ${answer}`);
                this.state.shouldContinue = false; // Break the loop

                console.log("✅ Task Complete. Returning final answer.");
                return answer;
            }
        }

        if (steps >= MAX_STEPS) {
            return "Error: Agent hit maximum step limit.";
        }

        return "No answer generated.";
    }
}

// ==========================================
// 5. EXECUTION (Simulating a Web App Request)
// ==========================================

/**
 * Main entry point simulating a Next.js API route handler.
 */
async function main() {
    // Simulate an incoming request from a SaaS frontend
    const userQuery = "What is the status of my subscription?";

    const agent = new ReActAgent(userQuery);
    const result = await agent.run();

    console.log("\n========================================");
    console.log("Final Output to Client:", result);
    console.log("========================================");
}

// Execute the simulation
main().catch(console.error);

Detailed Line-by-Line Explanation

1. Type Definitions & Interfaces

  • AgentState: This is the single source of truth for the agent during a session. In a real SaaS app, this might be stored in a Redis cache or a database session.
    • input: The original user query.
    • conversationHistory: A log of all interactions (user inputs, tool outputs, reasoning steps). This is crucial for the LLM to maintain context.
    • context: A key-value store for data retrieved from tools. This separates data from chat history.
    • shouldContinue: A boolean flag controlling the while loop.
  • Tool: Defines the contract for any external capability. This adheres to the Single Responsibility Principle; each tool does one specific thing (e.g., checking a subscription).

2. Mock Tools (The "Act" Phase)

  • checkSubscriptionTool: We simulate a database call.
    • Why mock? To keep the example self-contained and runnable without setting up a real database.
    • execute function: This represents the "Action" in ReAct. It takes arguments (parsed from the LLM's output) and returns a Promise (simulating async I/O).

3. Mock LLM (The "Reason" Phase)

  • mockLLMReasoning: In production, this would be a call to an LLM API (OpenAI, Anthropic) with a system prompt.
    • Decision Format: We return a string like TOOL:name:args or FINAL:answer. This is a simplified version of structured output (JSON) often used in LangChain/LangGraph.
    • Logic: It checks state.context. If empty, it decides to call a tool. If data exists, it synthesizes a final answer. This demonstrates the "Reasoning" step.

4. The ReAct Agent (The Orchestrator)

  • ReActAgent Class: Encapsulates the state and the loop logic.
  • run() Method:
    • Safety Guard: let steps = 0 and MAX_STEPS prevents infinite loops—a common issue when agents get stuck in repetitive reasoning.
    • The Loop (while): This is the heart of the ReAct pattern.
    • Reason: Calls mockLLMReasoning.
    • Act/Observe:
      • It parses the string returned by the LLM.
      • It finds the matching tool in the tools array.
      • It executes the tool.
      • State Update: Crucially, it updates this.state.context with the result. This ensures the next iteration of the loop has access to the new information.
    • Terminate: If the LLM returns FINAL:, the loop breaks (shouldContinue = false), and the answer is returned.

5. Execution

  • main: Simulates a web request. It initializes the agent with a user query and logs the final output.

Common Pitfalls in JavaScript/TypeScript Agents

When implementing ReAct patterns in production SaaS environments, watch out for these specific issues:

  1. Infinite Loops (The "Stuck Agent" Syndrome)

    • Issue: The LLM keeps requesting the same tool or fails to generate a final answer, causing the while loop to run indefinitely.
    • Fix: Always implement a hard MAX_STEPS limit (as shown in the code) and a timeout mechanism for the while loop.
  2. JSON Parsing Hallucinations

    • Issue: LLMs often output valid text but invalid JSON when asked to provide arguments for a tool. JSON.parse(argsStr) will throw a syntax error.
    • Fix: Use Zod or JSON Schema validation libraries. Wrap parsing in a try/catch block. If parsing fails, feed the error back into the LLM (or a fallback logic) to self-correct.
  3. Async/Await Race Conditions in State

    • Issue: In complex multi-agent systems, if the state object is mutated concurrently by multiple async tool calls, data can be overwritten or lost.
    • Fix: Treat AgentState as immutable within the loop. Create a new state object in every iteration (e.g., const newState = { ...oldState, context: newContext }) rather than mutating properties directly.
  4. Vercel/AWS Lambda Timeouts

    • Issue: ReAct loops can take time (LLM latency + tool execution). Serverless functions (like Vercel Edge or AWS Lambda) have strict timeouts (e.g., 10s for Edge, 60s for standard).
    • Fix: For long-running agents, do not await the full loop in the API response. Instead:
      • Start the agent asynchronously.
      • Return an immediate 202 Accepted response to the client.
      • Use WebSockets or Server-Sent Events (SSE) to stream the progress, or store the final result in a database for the client to poll.
  5. Context Window Overflow

    • Issue: Storing every step in conversationHistory (as we did in the example) will eventually exceed the LLM's token limit.
    • Fix: Implement summarization. Before sending the state to the LLM for reasoning, summarize older steps or use a sliding window approach to keep only the most recent N steps.

The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon


Loading knowledge check...



Code License: All code examples are released under the MIT License. Github repo.

Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.

All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.