Engineering the Agentic Stack · Part 1

AI Agent Reasoning Loops: ReAct, ReWOO, Plan-and-Execute

Article update

Originally published on 31 January 2026. Reviewed and updated on 6 September 2026. The update focuses on newer model capabilities and reasoning APIs, with revised examples and source links.

An agent reasoning loop is the control flow that decides when a model plans, calls a tool, reads the result, and stops. For an engineer building an agent, that choice is also a budget: it determines how often the model runs, how much history each call carries, and whether a surprising result can change the next action.

This article compares ReAct, ReWOO, and Plan-and-Execute through a LangGraph Market Analyst Agent I built. You will leave with a routing rule and implementation shapes to adapt, rather than three names to add to a diagram.

The loop is the innermost layer of the series. Memory, tools, security, runtime, and checks before a task is declared done surround it; they do not replace it.

For the short framework comparison, see Best AI Agent Frameworks in 2026.

The reasoning loop decides what to do next. It does not store state, execute tools, or authorize side effects.

The harness is the control program between the model and the machine. It assembles prompts from stored state (Part 2), defines the actions the model may name (Part 3), authorizes calls (Part 4), and checks evidence before declaring a task finished (Part 6). They are separate engineering problems, but one turn passes through all four.

The runtime (Part 5) supplies the session log, sandbox, checkpoint store, and traces that outlive one worker process.

Where each part of the Engineering the Agentic Stack series sitsWhere each part of the Engineering the Agentic Stack series sits

Each post stands alone. Together they move from the loop outward.


Start with the failure boundary

A good prompt does not settle the control-flow question. The patterns differ in how much work is fixed before the first tool call. That determines model-call count, when a bad plan becomes visible, and whether an unexpected tool result can redirect the run.

Three AI agent reasoning patterns

ReAct, ReWOO, and Plan-and-Execute compared by when evidence may change the planReAct, ReWOO, and Plan-and-Execute compared by when evidence may change the plan

ReAct: decide after every observation

ReAct (Yao et al., 2022), short for Reason + Act, keeps the next decision close to the latest observation:

The original paper uses explicit Thought, Action, and Observation text. Modern native tool APIs expose proposed calls and tool results; a visible thought is not required. Interleaved thinking is a separate model/API capability. In the historical prompting pattern:

  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 reads each tool result before choosing the next actionReAct reads each tool result before choosing the next action

This gives ReAct its useful properties:

  • In the paper’s PaLM-540B HotpotQA manual sample, Wikipedia observations produced fewer hallucinated facts than chain-of-thought prompting.
  • The agent can change strategy on the fly based on what it just saw.
  • The tool-call and observation history gives you a concrete execution trace.

The same loop has costs:

  • In a naive full-history implementation without caching, each turn processes the growing history again. Prompt caching changes input cost and processing time, but not context-window occupancy or stale observations. Measure cached and uncached tokens separately; summarization and truncation discard context.
  • Wasteful when the tool calls could have been planned upfront, which is the niche ReWOO fills.
  • Without a stop condition or step limit, the loop can run indefinitely.

Use it for exploratory tasks, debugging, and work where you cannot predict the next action.

ReWOO: compile the tool graph first

ReWOO (Reasoning WithOut Observation) separates planning from execution. The planner writes the complete tool sequence in one pass, using placeholders for values that only exist after execution.

  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 and fills in the placeholders. The paper’s worker follows the plan; the implementation later in this article adds dependency-aware parallel batches for ready steps.
  3. Solver: a final LLM call takes the gathered observations and writes the answer.

ReWOO plans the dependency graph before tools runReWOO plans the dependency graph before tools run

That separation provides:

  • Fewer repeated model calls than ReAct when the initial plan remains valid.
  • Less repeated prompt history than an interleaved full-history loop. Tool latency still depends on how the worker schedules calls.
  • The planner can be fine-tuned on its own, with no live environment.

It also creates a hard boundary. In the paper’s HotpotQA stress test, every tool returned No evidence found; ReWOO lost less accuracy than ReAct because the failed observations did not send its planner into another loop. That is relative robustness, not an execution recovery policy. An implementation still has to decide whether a tool error becomes solver evidence, triggers a retry, or aborts the run. ReWOO fits predictable workflows; it does not re-plan around a bad initial graph on its own.

Use it for quick snapshots, status checks, and dashboards whose tool behavior is predictable.

Plan-and-Execute: decompose, then react locally

Plan-and-Solve prompting describes a prompting method that first creates a plan and then solves the subtasks. A related tool-orchestration pattern is commonly called Plan-and-Execute. LangChain’s Plan-and-Execute guide documents that pattern:

  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 one at a time. Once tools are involved, each sub-task usually runs as its own small ReAct loop, so the executor can still react to what a tool returns even though the overall plan is fixed.

The original paper focused on zero-shot prompting. In a tool-using implementation, the orchestration pattern can execute the planned steps sequentially and use different models for planning and execution. That model split is an implementation choice, not a result established by the Plan-and-Solve paper.

Optional Plan-and-Execute replanning variant: each step uses feedback and a replanner may revise the overall planOptional Plan-and-Execute replanning variant: each step uses feedback and a replanner may revise the overall plan

The figure includes an optional replanning branch. The teaching graph later in this article does not use it: feedback can change work inside one step, but not the remaining plan.

The pattern is useful because it provides:

  • Hierarchical reasoning that mirrors how a human expert breaks down a project.
  • An explicit replanning edge can pause and reassess after an unexpected step result.
  • Model specialization. The planner can be expensive, the executor can be cheap.
  • With a checkpointer configured, each completed step can become a resume boundary.

Its costs are:

  • More model round trips than ReWOO when each step contains its own ReAct loop.
  • More state to manage.
  • Overkill for one-shot queries.

Use it for complex analysis and research that need a final synthesis.

Choose by where the plan can fail

FeatureReAct (2022)Plan-and-Execute (2023)ReWOO (2023)
Core philosophyImproviser: decide the next move from the last result, one call at a time.Architect: build a full blueprint, execute it, then review.Optimizer: compile a dependency graph, then batch the calls that are ready.
WorkflowIterative loop: Thought → Action → Observation.Two-stage: Phase 1 (Planning), Phase 2 (Execution).Decoupled: Planner writes a graph of tool calls; Worker runs them; Solver composes the answer.
AdaptabilityHighest: can change direction after every single tool call.Each step can respond to its result. An optional replanner can revise later steps.Lowest: the planner’s script runs to completion; nothing re-plans mid-run.
EfficiencyA naive full-history loop repeats more input tokens; context management can cap that growth.Each step’s ReAct loop can start from a short context instead of the whole run’s history; a replanner adds planning calls when enabled.Fewer model calls; this article’s worker also batches dependency-ready tools.
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 routing aid, not a benchmark. Use ReAct when a tool result may change the next action. Use Plan-and-Execute when the task splits into steps but each step still needs feedback. Add a replanner only when one result must change later steps; the teaching graph below does not have one. Use ReWOO when every tool dependency is known before execution. Its dependency graph lets the teaching worker run ready calls in parallel and detect a graph that cannot progress. With the companion’s execute_tool helper, caught tool errors become strings passed to the solver. An exception that escapes the helper aborts the worker. Neither path supplies retries or replanning. Measure all three with your model, tool latency, task set, and retry policy before optimizing for call count.

What changes with current models and harnesses

These patterns describe decisions outside the model. A model that reasons between tool calls still needs a program to run those calls, stop the loop, and authorize effects. On Claude Sonnet 5, adaptive thinking is enabled by default and its text is omitted by default. An empty thinking block does not mean the model skipped reasoning. Preserve complete signed thinking blocks when returning tool results; rebuilding the conversation from visible text loses that continuity. Compare reasoning effort and billed output tokens as well as the number of model calls.

For a new LangChain implementation, start with create_agent. This self-contained wiring example uses the current Sonnet identifier and a local fixture tool. Set ANTHROPIC_API_KEY and install langchain plus langchain-anthropic before invoking it; that invocation makes paid model requests.

from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic

def lookup_fixture_company(ticker: str) -> str:
    """Look up a company name in a tiny local fixture, not live market data."""
    return {"NVDA": "NVIDIA", "AMD": "AMD"}.get(
        ticker.strip().upper(), "No company in the fixture"
    )

agent = create_agent(
    model=ChatAnthropic(model="claude-sonnet-5", max_tokens=4096),
    tools=[lookup_fixture_company],
    system_prompt="Use the fixture for company names. Do not invent market data.",
)
result = agent.invoke({
    "messages": [{"role": "user", "content": "Which company is NVDA in the fixture?"}]
})
print(result["messages"][-1].content)

This is an observation-driven tool loop. No separate planner is needed for that one lookup. I leave sampling overrides out of the example: a model upgrade also requires checking accepted parameters, response blocks, and structured-output behavior. The example was checked offline for imports and construction, not evaluated with live inference.

If the job needs isolated subagent contexts, files, and automatic context management, Deep Agents packages those capabilities around the same tool loop. That is a harness choice, not a fourth reasoning algorithm. Compare a single agent with delegation on tasks that actually split into independent work; include coordination cost and lost context in the result.

A worked example: the Market Analyst Agent

The Market Analyst Agent makes the distinction concrete. One codebase uses all three patterns for market research, and a router chooses between a deep-research path and a flash-briefing path. The excerpts below are abridged teaching variants of commit b4e769a: the worker below adds dependency-ready parallel batches and raises when the graph cannot progress. Its execute_tool is the companion helper, which catches tool exceptions and returns error strings for the solver. Only exceptions that escape that helper abort a future. This teaching adaptation does not add retries or recovery.

It uses LangGraph for orchestration. A node is a Python function that returns fields to update in shared state. An edge declares the next node and can call a routing function. LangGraph merges updates and checkpoints at super-step boundaries: one node or a batch of parallel nodes. Pending writes preserve successful sibling results when another node fails. The three patterns share one state object, so routing does not require three separate schemas:

Market Analyst Agent routes one request into two reasoning loops with shared stateMarket Analyst Agent routes one request into two reasoning loops with shared state

The diagram isolates routing and draft creation. It omits the shared evaluator and the human approval before publishing, shown later, so the two reasoning loops remain legible.

State definition

The Python blocks below are integration excerpts, not standalone scripts: they share state types, node helpers, and framework imports from the companion. The repository’s isolated example runner skips them; offline contract checks cover the state, proposed calls, message IDs, and resume behavior.

The state schema carries the fields both modes need:

from typing import Literal

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

    # Final report. Both paths write this field, then the graph pauses before
    # publishing (see interrupt_before below), so a human signs off on a draft
    # a fresh-context evaluator has already voted on.
    draft_report: DraftReport | None = None
    report_approved: bool = False
    evaluator_verdict: Literal["pass", "fail", "needs_human"] | None = None
    evaluator_reasons: list[str] = Field(default_factory=list)

Pattern 1: Plan-and-Execute implementation

Plan-and-Execute fits multi-step synthesis. A planner writes the high-level steps, then a ReAct loop executes each step and reacts to tool results.

The following historical companion excerpts retain claude-sonnet-4-5-20250929 and the older create_react_agent API so they remain comparable with the linked commit. The current starting point is the create_agent example above. Migrating the full graph requires testing its planner schemas, inner message handling, routing, and resume behavior together; changing the model string alone is not that migration.

The implementation keeps four boundaries visible:

  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 response shape. Execution needs a separate plan check.
  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)

    # Pull the request out of the message history
    human = [m for m in state.messages if isinstance(m, HumanMessage)]
    last_user_message = human[-1].content if human else "Analyze the market"

    # 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 rejects malformed fields. Before execution, also require a nonempty, bounded step list with unique step numbers and usable descriptions; this abridged schema does not enforce those conditions. The companion can still accept an empty plan and fail when its executor indexes the first step.

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 model/tool-result loop runs inside create_react_agent; the factory does not promise an exposed reasoning transcript.
  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.
# The five market-data tools the ReAct agent chooses from here. The repo's
# TOOLS list carries four more — a skill loader, two CLI wrappers, and a
# restricted in-process Python evaluator — covering three of the five tool
# modalities Part 3 compares. MCP is the fourth, and it lives in a sidecar
# rather than in this list.
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
    # This companion example pins LangGraph's deprecated create_react_agent API.
    # Current LangChain guidance recommends create_agent instead:
    # https://reference.langchain.com/python/langgraph.prebuilt/chat_agent_executor/create_react_agent
    # The observable loop is a proposed call, its result, and the next decision.
    # A visible reasoning block depends on the model and API configuration.
    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 synthesized answer
    )

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

Pattern 3: ReWOO for fast snapshots

For a quick briefing, ReWOO removes model calls from the execution phase. Independent tools run in parallel; dependent tools wait for their prerequisites. The planner emits the tool graph up front, and the worker executes it without asking the model what to do next.

The shape of it:

  1. Three phases (Planner → Worker → Solver). The worker does not ask the model what to do next or re-plan.
  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 (writes a typed proposed-call plan 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     # Requested tool name; validate it against a registry in production
    tool_args: dict    # Proposed arguments; may contain refs like {"price": "#E1"}
    depends_on: list[str] = []  # Declared ordering; cross-check it against #E placeholders
    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, it creates typed proposed tool calls.
    Production code validates them before execution; this teaching worker does not.
    """

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

    # Schema-Guided Reasoning validates the planner response shape.
    # It does not validate a tool name, its arguments, or its dependencies.
    structured_llm = llm.with_structured_output(ReWOOPlanOutput)

    ticker = state.research_data.ticker if state.research_data else "UNKNOWN"
    human = [m for m in state.messages if isinstance(m, HumanMessage)]
    query = human[-1].content if human else f"Analyze {ticker}"

    # 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"""),
    ])

    # Store the typed proposed-call plan.
    # This teaching worker sends ready steps directly to execute_tool.
    return {"rewoo_plan": result.steps}

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

def rewoo_worker_node(state: AgentState) -> dict:
    """Execute dependency-ready tools in parallel batches (no LLM calls).

    Independent tools share a batch. Dependent tools wait until their
    prerequisites complete. The worker follows the dependency graph and
    does not add LLM calls.
    """

    results = {}        # Results keyed by step_id (e.g., "#E1": "$150.23")
    updated_steps = []  # Plan steps with their result field filled in
    pending = {step.step_id: step for step in state.rewoo_plan}

    # Keep scheduling dependency-ready batches until the graph is complete.
    # This handles chains even when the planner does not list them topologically.
    with ThreadPoolExecutor(max_workers=5) as executor:
        while pending:
            ready = [
                step for step in pending.values()
                if all(dep in results for dep in step.depends_on)
            ]
            if not ready:
                unresolved = ", ".join(pending)
                raise ValueError(f"Unresolvable ReWOO dependencies: {unresolved}")

            futures = {
                executor.submit(execute_tool, step, results): step
                for step in ready
            }
            for future in as_completed(futures):
                step = futures[future]
                results[step.step_id] = future.result()
                updated_steps.append(step.model_copy(update={"result": results[step.step_id]}))
                del pending[step.step_id]

    # State update: restore the planner's order (sorting on step_id would put
    # "#E10" before "#E2") and hand the filled-in plan to the Solver
    plan_order = {s.step_id: i for i, s in enumerate(state.rewoo_plan)}
    return {"rewoo_plan": sorted(updated_steps, key=lambda s: plan_order[s.step_id])}

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
    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)
    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}"),
    ])

    # FlashBriefingOutput and DraftReport carry the same fields; the state
    # schema expects DraftReport, so convert before returning.
    return {"draft_report": DraftReport(**result.model_dump())}

The planner writes proposed calls, the worker fills their placeholders, and the solver receives the results. The schema only checks the response shape. Before dispatch, reject empty or oversized plans, duplicate IDs, missing dependencies, and cycles. Duplicate IDs silently collapse in the pending dictionary above. Check tool names and arguments against a registry, recursively inspect placeholders inside nested lists and objects, cross-check them against depends_on, and reject unresolved references before the affected call. This teaching worker skips those checks and sends ready steps to the companion’s execute_tool, including its error-as-evidence behavior. The solver must treat an error string as missing evidence, not a successful lookup. If its graph cannot progress, it stops before the solver; there is no retry or replanning path. Retrying the same state only repeats the same plan, so replanning needs a failure result, a conditional edge, and a limit on attempts.

Where each pattern calls the model

PatternLLM calls during executionState updatesKey code pattern
Plan-and-Execute1 for planning + a ReAct loop per step (several calls each) + 1 for the reportSequential step completionplanner_node() → loop: executor_node()reporter_node()
ReAct (within each step)Multiple per step (thought-action cycles)Inner transcript only; outer graph records completed-step resultsPinned companion uses deprecated create_react_agent()
ReWOO1 for planning + 0 during execution + 1 for synthesisDependency-aware tool batchesrewoo_planner_node()rewoo_worker_node()rewoo_solver_node()

The important difference is the planner’s output. It determines how much discretion the executor retains:

  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_snapshot"
        ),
        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 each step is its own ReAct loop, so a step costs several model calls, not one.

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

    # A standalone full-history ReAct loop carries messages from earlier turns.
    messages = [
        HumanMessage(content="Execute Step 1: Get current price"),
        AIMessage(content="", tool_calls=[{
            "id": "1",
            "name": "get_stock_snapshot",
            "args": {"ticker": "NVDA"},
            "type": "tool_call",
        }]),
        ToolMessage(tool_call_id="1", content="$132.45"),
        AIMessage(content="Now I need metrics..."),
        # ... agent continues until step complete
    ]

    In a standalone full-history ReAct loop, every model call carries a history that grows for the whole task. Cache hits can reduce repeated computation and input charges; a production loop may also summarize or truncate it. The Plan-and-Execute example starts each ReAct invocation with the current step and a digest of earlier findings. It keeps the completed result in plan, not the inner tool-call transcript.

  3. ReWOO creates explicit, typed proposed tool calls:

    # Planner output (list of ReWOOPlanStep objects)
    rewoo_plan = [
        ReWOOPlanStep(
            step_id="#E1",
            description="Read the current price and valuation snapshot",
            tool_name="get_stock_snapshot",
            tool_args={"ticker": "NVDA"}
        ),
        ReWOOPlanStep(
            step_id="#E2",
            description="Find recent NVDA earnings news",
            tool_name="search_news",
            tool_args={"query": "NVDA earnings", "max_results": 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:

  • Plan-and-Execute: state moves through plancurrent_step_indexresearch_data.
  • ReAct within this Plan-and-Execute graph: the inner agent produces one step’s transcript. The outer graph keeps plan, current_step_index, and the completed-step results.
  • ReWOO: state moves through rewoo_plan, with result fields filled in by the worker.

Wire both routes into one graph

The graph has two user-facing routes over one AgentState: deep research uses Plan-and-Execute with a ReAct loop inside each step, while flash briefing uses ReWOO. ReAct is an execution primitive here, not a third route.

This implementation has no replanner: it runs the initial plan to completion. Adding replanning would require an edge from executor back to planner and a rule for when a surprising result justifies another model call.

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)
    builder.add_node("evaluator", evaluator_node)
    builder.add_node("publish", publish_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", "evaluator")

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

    # Both paths run the same evaluator before the human approval step.
    builder.add_edge("evaluator", "publish")
    builder.add_edge("publish", END)

    return builder.compile(
        checkpointer=checkpointer,
        # Human-in-the-loop pause: the reporter (or ReWOO solver) writes a
        # draft. A separate model session reads that draft and records its
        # assessment. The graph then stops before publishing, whichever
        # assessment it produced, so a human can review both the draft and
        # assessment. Part 6 explains how the program decides a run is complete.
        interrupt_before=["publish"],
    )

Automatic pattern selection with a router

The router maps request shape to route. Schema-Guided Reasoning constrains the classifier’s output:

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."""

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

With this router, “current price” goes to ReWOO and “investment thesis” goes to Plan-and-Execute. The default is deep research when the request is ambiguous. Before putting the router in front of users, compare both routes with a fixed workflow on the same tasks, tools, evidence, and budget. Repeat stochastic trials and count every started attempt. Track task success, incorrect acceptance, cached and uncached tokens, wall time, and recovery after tool errors or misleading observations. These patterns are control-flow choices, not an adoption ranking; Anthropic’s engineering account likewise recommends starting with simple composable workflows.

The full companion implementation, including the router and shared state, is at the pinned Market Analyst Agent commit.

The next layer is memory

Part 2, AI Agent Memory Architecture, 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