AI Agent Reasoning Loops in 2026: ReAct vs ReWOO vs Plan-and-Execute

Part 1 of the Engineering the Agentic Stack series

An agent reasoning loop controls how a system plans, calls tools, reads results, and decides when to stop. That control flow shapes cost, latency, and recovery after a tool returns something unexpected.

This post compares the three AI agent loop patterns worth knowing in 2026: ReAct, ReWOO, and Plan-and-Execute. The running example is a Market Analyst Agent I built in LangGraph, with the full code on GitHub.

This is the inner layer of the series. The later parts add the state the loop resumes from, the tools it can call, the policy around those calls, the runtime that keeps a run alive, and the harness checks that decide whether the work is actually finished.

TL;DR: ReAct is flexible but expensive, ReWOO is fast when the workflow is predictable, and Plan-and-Execute fits multi-step analysis. A production AI agent can route between reasoning loops per request, using shared state and checkpointed LangGraph nodes so each task gets the loop that matches its shape.


The loop controls cost, latency, and recovery

A useful agent needs more than a good prompt. Its control graph must connect reasoning, tools, and memory while handling failures.

The reasoning loop controls when the model plans, calls a tool, reads the result, and stops. A poor choice creates unnecessary model calls and latency. It can also leave the agent unable to recover from an unexpected tool result.

Three AI agent reasoning patterns

Reasoning Patterns Comparison

ReAct: think, act, observe, repeat

ReAct (Yao et al., 2022) is the original pattern for interactive agents. It runs a loop:

  1. Thought: the agent generates a “thought” to break down the goal and plan the next step.
  2. Action: based on the thought, it calls a tool.
  3. Observation: the agent reads the result, which updates its understanding for the next thought.

ReAct Pattern

Pros:

Cons:

Best for: exploratory tasks, debugging, situations where you can’t predict what comes next.

ReWOO: plan everything upfront

ReWOO (Reasoning WithOut Observation) is a more efficient cousin of ReAct. The trick is decoupling reasoning from tool execution: instead of stopping to observe each action, ReWOO plans the entire sequence of tool calls in one pass.

  1. Plan: one LLM call writes the full plan of tool calls, using variable placeholders (#E1, #E2) for outputs that don’t exist yet.
  2. Worker: a non-LLM executor runs the planned tools in sequence or in parallel, filling in the placeholders.
  3. Solver: a final LLM call takes the gathered observations and writes the answer.

ReWOO Pattern

Pros:

Cons:

Best for: quick snapshots, status checks, dashboards. Anything where the tools behave predictably.

Plan-and-Execute: a hybrid

Plan-and-Execute sits between the two. The paper sketches a simple split that most modern agents have adopted:

  1. Planning phase: the agent first generates a plan that breaks the task into smaller sub-tasks.
  2. Execution phase: the agent then carries out those sub-tasks.

The original paper focused on zero-shot prompting. Modern frameworks like LangGraph have grown it into a full orchestration pattern with sequential execution and per-step model choice (a strong reasoning model for planning, a cheaper one for execution).

Plan-and-Execute Pattern

Pros:

Cons:

Best for: complex multi-step analysis, research tasks, anything that needs synthesis at the end.

Choose by where the plan can fail

FeatureReAct (2022)Plan-and-Execute (2023)ReWOO (2023)
Core philosophyImproviser: act, then decide what to do next based on the result.Architect: build a full blueprint, execute it, then review.Optimizer: write a “script” with variables and run it all at once.
WorkflowIterative loop: Thought → Action → Observation.Two-stage: Phase 1 (Planning), Phase 2 (Execution).Decoupled: Planner creates a graph of tool calls; Worker runs them.
AdaptabilityHighest: can change direction after every single tool call.Medium: typically re-plans only after a set of steps is completed.Lowest: usually follows the initial script unless the Solver fails.
EfficiencyLow: high token usage; must re-read entire history for every step.Medium: saves tokens by not “re-thinking” during execution.High: minimal LLM calls; can parallelize tool execution for speed.
Best forOpen-ended exploration or tasks where results are unpredictable.Long-horizon tasks that require a steady goal (e.g., writing a paper).Structured, repeatable workflows (e.g., checking weather in 5 cities).

The table is a starting point, not a benchmark result. Use ReAct when each observation may change the next action. Use Plan-and-Execute when the overall task can be decomposed but individual steps still need feedback. Use ReWOO when dependencies are known before execution and a failed input can stop its downstream calls. Measure all three on your own tool latency, model, task set, and retry policy before choosing on cost alone.

A worked example: the Market Analyst Agent

To make this concrete I built a Market Analyst Agent that uses all three patterns in one codebase, on a market research task.

It uses LangGraph for orchestration. All three patterns share the same state object, so the router can pick which one to run per request:

Plan-and-Execute Architecture

State definition

The shared state captures everything the agent needs across modes:

class PlanStep(BaseModel):
    """A single step in the research plan."""
    step_number: int
    description: str
    tool_hint: str | None = None
    completed: bool = False
    result: str | None = None

class UserProfile(BaseModel):
    """Structured user context loaded from long-term memory."""
    risk_tolerance: str | None = None
    investment_horizon: str | None = None

class AgentState(BaseModel):
    """Main state for the Market Analyst Agent graph."""

    # Identity and profile context for memory-backed personalization
    user_id: str
    user_profile: UserProfile = Field(default_factory=UserProfile)

    # Message history with LangGraph's add_messages reducer
    messages: Annotated[list, add_messages] = Field(default_factory=list)

    # Execution mode (set by router)
    execution_mode: ExecutionMode | None = None

    # Plan-and-Execute state
    plan: list[PlanStep] = Field(default_factory=list)
    current_step_index: int = 0

    # ReWOO state
    rewoo_plan: list[ReWOOPlanStep] = Field(default_factory=list)

    # Research results
    research_data: ResearchData | None = None

    # HITL output
    draft_report: DraftReport | None = None
    report_approved: bool = False

Pattern 1: Plan-and-Execute implementation

Plan-and-Execute is the right fit for tasks that need multi-step synthesis. The trick is keeping planning and execution separate: a strong model for the upfront plan, then a ReAct loop to execute each step with room to react to tool results.

How it lines up with the pattern:

  1. One upfront planning phase. A single LLM call produces the whole plan as a list of step descriptions.
  2. Schema-guided output, which validates the plan before execution.
  3. No tool execution yet. The planner only decides what to do, not how.
  4. Human-readable steps. Each step is text that an executor will interpret.
# System prompt guides the LLM to think like a research analyst
# creating a strategic plan, not immediate tool calls
PLANNER_SYSTEM_PROMPT = """You are a senior investment research analyst.
Break down stock analysis requests into 4-6 research steps covering:
1. Current price and basic metrics
2. Recent news and announcements
3. Competitor analysis (if relevant)
4. Financial health assessment
5. Risk factors
6. Investment thesis synthesis

Output as JSON with step_number, description, and tool_hint."""

# Schema-Guided Reasoning: Enforce structure with Pydantic
class PlanOutput(BaseModel):
    """Structured output for the planner."""

    steps: list[PlanStep] = Field(description="Research steps to execute")
    ticker: str = Field(description="The stock ticker being analyzed")

def planner_node(state: AgentState) -> dict:
    """Generate a research plan from the user's request.

    This is Phase 1 of Plan-and-Execute: creating the high-level strategy.
    """

    # Use a powerful model for strategic planning
    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)

    # Ask for a typed plan and validate it before execution.
    # The API can still fail, so production code also handles that exception.
    structured_llm = llm.with_structured_output(PlanOutput)

    # Context from long-term memory personalizes the plan
    profile_context = f"""
User Profile:
- Risk Tolerance: {state.user_profile.risk_tolerance}
- Investment Horizon: {state.user_profile.investment_horizon}
"""

    # Single LLM call creates the complete plan
    result: PlanOutput = structured_llm.invoke([
        SystemMessage(content=PLANNER_SYSTEM_PROMPT + profile_context),
        HumanMessage(content=f"Create a research plan for: {last_user_message}"),
    ])

    # State update: Store the plan and initialize tracking
    return {
        "plan": result.steps,           # The sequential steps to execute
        "current_step_index": 0,        # Start at step 0
        "research_data": ResearchData(ticker=result.ticker),  # Initialize data container
    }

That llm.with_structured_output(PlanOutput) line is Schema-Guided Reasoning (SGR), which I covered in a previous post. The schema lets the application reject a malformed plan before LangGraph uses the validated fields to drive conditional edges.

Pattern 2: ReAct execution

Once the plan exists, the executor runs each step as its own ReAct loop. This is Phase 2: each step is small enough that a Thought-Action-Observation cycle stays focused, and the agent can react to whatever the tool returns.

How the ReAct part lines up:

  1. Iterative execution. One step at a time, with observation feedback.
  2. The Thought-Action-Observation loop runs inside create_react_agent.
  3. Previous step results get fed in as context for the current reasoning.
  4. The agent picks tools based on the step description.
  5. It can change approach mid-step based on what a tool returns.
# Tools available for the ReAct agent to choose from
TOOLS = [
    get_stock_snapshot,
    get_price_history,
    search_news,
    search_competitors,
    get_financials,
]

def executor_node(state: AgentState) -> dict:
    """Execute the current step using a ReAct agent.

    This is Phase 2 of Plan-and-Execute: adaptive execution of each planned step.
    Each step runs as a mini ReAct loop until completion.
    """

    # Get the current step from the plan
    current_step = state.plan[state.current_step_index]

    # Build context from what we've learned so far
    # This matters: each step builds on previous observations
    previous_context = ""
    for step in state.plan[:state.current_step_index]:
        if step.result:
            previous_context += f"\nStep {step.step_number}: {step.result}\n"

    # Create a ReAct agent for this step
    # LangGraph's create_react_agent implements the full Thought-Action-Observation loop:
    # 1. Agent generates a "thought" about what tool to call
    # 2. Agent calls the tool ("action")
    # 3. Tool returns result ("observation")
    # 4. Agent decides: call another tool or finish
    react_agent = create_react_agent(
        model=ChatAnthropic(model="claude-sonnet-4-5-20250929"),
        tools=TOOLS,
    )

    # Invoke the ReAct loop for this single step
    # The agent will loop internally until it completes the step
    result = react_agent.invoke({
        "messages": [
            SystemMessage(content=EXECUTOR_SYSTEM_PROMPT),
            HumanMessage(content=f"""Execute Step {current_step.step_number}:
{current_step.description}

Ticker: {state.research_data.ticker}
Previous findings: {previous_context}"""),
        ]
    })

    # Extract the final answer from the ReAct agent's message history
    # The last message contains the synthesis after all tool calls
    updated_plan = list(state.plan)
    updated_plan[state.current_step_index] = PlanStep(
        step_number=current_step.step_number,
        description=current_step.description,
        completed=True,
        result=result["messages"][-1].content,  # Final observation
    )

    # State update: Mark step complete and advance to next
    return {
        "plan": updated_plan,
        "current_step_index": state.current_step_index + 1,
    }

That’s the core Plan-and-Execute flow: a powerful model writes the plan, then ReAct executes each step with full adaptability.

Pattern 3: ReWOO for fast snapshots

For quick briefings, ReWOO skips the interleaved reasoning and runs the tools in parallel. The planner emits a compiled script of tool calls upfront, and the worker runs it without any further LLM involvement.

The shape of it:

  1. Three phases (Planner → Worker → Solver), no loops.
  2. Tool calls reference #E1, #E2 placeholders for results that don’t exist yet.
  3. No LLM during execution. The worker just runs tools.
  4. Independent tools run in parallel.
  5. One synthesis call at the end, over all the data at once.

Phase 1: ReWOO planner (creates the complete execution graph upfront)

class ReWOOPlanStep(BaseModel):
    """A step in the ReWOO plan with variable placeholders.

    Key difference from Plan-and-Execute's PlanStep:
    - Contains actual tool_name and tool_args (not just description)
    - Uses variable references (#E1) for dependencies
    """
    step_id: str  # e.g., "#E1" - becomes a variable
    description: str
    tool_name: str     # Exact tool to call
    tool_args: dict    # May contain variable refs like {"price": "#E1"}
    depends_on: list[str] = []  # For dependency ordering
    result: str | None = None

class ReWOOPlanOutput(BaseModel):
    """Structured output for ReWOO planner."""
    steps: list[ReWOOPlanStep] = Field(description="Planned tool calls with variables")

def rewoo_planner_node(state: AgentState) -> dict:
    """Generate a complete plan of tool calls upfront.

    This is the key difference from Plan-and-Execute: instead of creating
    human-readable step descriptions, we create EXACT tool calls that
    the worker will execute blindly.
    """

    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)

    # Schema-Guided Reasoning ensures valid tool call specifications
    structured_llm = llm.with_structured_output(ReWOOPlanOutput)

    ticker = state.research_data.ticker if state.research_data else "UNKNOWN"

    # Single LLM call to plan ALL tool executions
    result: ReWOOPlanOutput = structured_llm.invoke([
        SystemMessage(content=REWOO_PLANNER_PROMPT),
        HumanMessage(content=f"""Create a ReWOO plan for: {query}

Ticker: {ticker}

Output tool calls with:
- step_id: Variable name (#E1, #E2, etc.)
- description: What this accomplishes
- tool_name: Exact tool from the list
- tool_args: Dictionary of arguments
- depends_on: List of step_ids this depends on"""),
    ])

    # State update: Store the complete execution plan
    # Worker will execute this without any LLM involvement
    return {"rewoo_plan": result.steps}

Phase 2: ReWOO worker (executes tools without LLM reasoning)

def rewoo_worker_node(state: AgentState) -> dict:
    """Execute all planned tools in parallel (no LLM calls).

    This is the key efficiency: Worker is "dumb" - it just runs tools
    according to the plan. No LLM calls = massive token savings.
    """

    results = {}  # Store results keyed by step_id (e.g., "#E1": "$150.23")

    # Execute ALL independent steps in parallel using ThreadPoolExecutor
    # This is where ReWOO gets its speed advantage
    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = {
            executor.submit(execute_tool, step): step
            for step in state.rewoo_plan
            if not step.depends_on  # Only independent tools for parallel batch
        }

        # Collect results as they complete
        for future in as_completed(futures):
            step = futures[future]
            results[step.step_id] = future.result()
            # No LLM reasoning here - just store the raw tool output

    # State update: Store results for the Solver phase
    return {"rewoo_plan": updated_steps}

Phase 3: ReWOO solver (synthesizes all results in one LLM call)

def rewoo_solver_node(state: AgentState) -> dict:
    """Synthesize all tool results into a flash briefing.

    This is the second efficiency gain: Instead of interleaving
    LLM calls with tool execution (like ReAct), we make ONE
    final synthesis call with all gathered data.
    """

    # Build context from ALL tool results at once
    tool_results = []
    for step in state.rewoo_plan:
        if step.result:
            tool_results.append(f"### {step.description}\n{step.result}")

    context = "\n\n".join(tool_results)

    # Single LLM call to synthesize everything
    structured_llm = llm.with_structured_output(FlashBriefingOutput)
    result = structured_llm.invoke([
        SystemMessage(content=REWOO_SOLVER_PROMPT),
        HumanMessage(content=f"Create a flash briefing from this data:\n\n{context}"),
    ])

    return {"draft_report": result}

ReWOO plans every tool call up front and stores dependencies in placeholders such as #E1 and #E2. The executor can run independent calls in parallel without asking the model what to do next. One final model call combines the results, which keeps predictable workflows cheap.

Where each pattern calls the model

The three patterns differ in when and how they call the LLM:

PatternLLM calls during executionState updatesKey code pattern
Plan-and-Execute1 for planning + 1 per stepSequential step completionplanner_node() → loop: executor_node()reporter_node()
ReAct (within each step)Multiple per step (thought-action cycles)Accumulated message historycreate_react_agent() loops internally until step complete
ReWOO1 for planning + 0 during execution + 1 for synthesisParallel tool completionrewoo_planner_node()rewoo_worker_node()rewoo_solver_node()

What changes between them is what the planner produces. That decides everything downstream.

  1. Plan-and-Execute creates human-readable step descriptions:

    # Planner output (list of PlanStep objects)
    plan = [
        PlanStep(
            step_number=1,
            description="Get current price and key financial metrics",
            tool_hint="get_stock_price"
        ),
        PlanStep(
            step_number=2,
            description="Search for recent news and earnings",
            tool_hint="search_news"
        ),
        # ... more steps
    ]

    The executor reads each description and decides which tools to call. Flexible, but it costs an LLM call per step.

  2. ReAct doesn’t have an upfront plan. It uses iterative reasoning:

    # No planning phase - ReAct works step-by-step with accumulated messages
    messages = [
        HumanMessage(content="Execute Step 1: Get current price"),
        AIMessage(content="I'll call get_stock_price"),
        ToolMessage(tool_call_id="1", content="$132.45"),
        AIMessage(content="Now I need metrics..."),
        # ... agent continues until step complete
    ]

    Multiple LLM calls per step adapt to observations and usually cost more than the two planned modes.

  3. ReWOO creates explicit, executable tool call specifications:

    # Planner output (list of ReWOOPlanStep objects)
    rewoo_plan = [
        ReWOOPlanStep(
            step_id="#E1",
            tool_name="get_stock_price",
            tool_args={"ticker": "NVDA"}
        ),
        ReWOOPlanStep(
            step_id="#E2",
            tool_name="search_news",
            tool_args={"query": "NVDA earnings", "limit": 5}
        ),
        # ... all tool calls planned upfront
    ]

    The worker runs blind, with no LLM involvement. All model calls live in the planner and solver, which makes the model-call count predictable.

Memory and state flow:

Putting it all together: wiring the graph

Here’s how the three patterns coexist in a single LangGraph system. They share one AgentState and live in one graph. A router picks the path per request, so this is one agent with three execution modes, not three agents in a trench coat.

LangGraph keeps the wiring declarative:

def create_graph(checkpointer=None):
    builder = StateGraph(AgentState)

    # Add nodes
    builder.add_node("router", router_node)
    builder.add_node("planner", planner_node)
    builder.add_node("executor", executor_node)
    builder.add_node("reporter", reporter_node)
    builder.add_node("rewoo_planner", rewoo_planner_node)
    builder.add_node("rewoo_worker", rewoo_worker_node)
    builder.add_node("rewoo_solver", rewoo_solver_node)

    # Define edges
    builder.add_edge(START, "router")
    builder.add_conditional_edges("router", route_after_router, {
        "planner": "planner",
        "rewoo_planner": "rewoo_planner",
    })

    # Deep Research path
    builder.add_edge("planner", "executor")
    builder.add_conditional_edges("executor", route_after_executor, {
        "executor": "executor",  # Loop back for more steps
        "reporter": "reporter",  # Done with plan
    })
    builder.add_edge("reporter", END)

    # Flash Briefing path (ReWOO)
    builder.add_edge("rewoo_planner", "rewoo_worker")
    builder.add_edge("rewoo_worker", "rewoo_solver")
    builder.add_edge("rewoo_solver", END)

    return builder.compile(
        checkpointer=checkpointer,
        interrupt_before=["reporter"],  # HITL pause for approval
    )

Automatic pattern selection with a router

To pick the right loop for each request, I added a router classifier. It uses Schema-Guided Reasoning to keep the classification reliable:

class ExecutionMode(str, Enum):
    """Execution mode for the agent."""

    DEEP_RESEARCH = "deep_research"  # Plan-and-Execute + ReAct (thorough)
    FLASH_BRIEFING = "flash_briefing"  # ReWOO (fast, token-efficient)

class RouterOutput(BaseModel):
    """Structured output for the router."""

    mode: ExecutionMode  # DEEP_RESEARCH or FLASH_BRIEFING
    ticker: str
    reasoning: str

ROUTER_SYSTEM_PROMPT = """Classify the user's request:

1. **deep_research**: Complex analysis requiring synthesis
   - Examples: "Analyze strategic risks", "investment thesis"

2. **flash_briefing**: Quick snapshots, simple data retrieval
   - Examples: "quick snapshot", "current price"

Default to deep_research if unclear."""

structured_llm = llm.with_structured_output(RouterOutput)

With this in place, users don’t pick a mode. The router routes “current price” to ReWOO and “investment thesis” to Plan-and-Execute on its own.

Key takeaways

  1. ReAct offers the most frequent feedback points, at the cost of repeated model calls.
  2. ReWOO reduces model round trips when the tools are reliable and dependencies are predictable.
  3. Plan-and-Execute fits complex analysis that can be decomposed before execution but still needs per-step feedback.
  4. A router can pick between them per request, so users don’t have to.
  5. The loop needs stored state to survive an interrupt or worker restart. That is the subject of Part 2.

The full implementation, including the router and the shared state, is in the Market Analyst Agent repository.

The next layer is memory

Part 2, AI Agent Memory Architecture in 2026, separates resumable checkpoints from cross-session knowledge and project documents. Without that state layer, the router and executor above only work while one process and one context window stay alive.

References


The Market Analyst Agent code is on GitHub if you want to read along.

Series: Engineering the Agentic Stack