Engineering the Agentic Stack · Part 1

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

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 acceptance checks 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:

  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, history is re-processed at every step, so latency and cost grow with the loop length. Summarization or truncation can cap that growth at the cost of discarded 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.

Plan-and-Execute keeps an overall plan while each step uses feedbackPlan-and-Execute keeps an overall plan while each step uses feedback

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.Medium: typically re-plans only after a set of steps is completed.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.Re-planning is occasional rather than per-step, and each step’s ReAct loop can start from a short context instead of the whole run’s history.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 an observation may change the next action. Use Plan-and-Execute when the task decomposes cleanly but each step still needs feedback. Use ReWOO when every tool dependency is known before execution. In the teaching implementation below, that dependency graph enables parallel batches and lets the worker detect a graph that cannot make progress. Its execute_tool helper is intentionally fail-fast; production code needs an explicit retry, fallback, or error-as-evidence policy. Measure all three with your model, tool latency, task set, and retry policy before optimizing for call count.

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 pinned companion catches a tool exception and passes an error string to its solver, while the worker shown here lets that exception abort and raises when its dependency graph cannot progress. The route and state shape are the same; the failure policy is deliberately explicit here.

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 can checkpoint after every node, making a run resumable. 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 publish gate shown later so the two reasoning loops remain legible.

State definition

The state schema carries the fields both modes need:

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

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 code below pins claude-sonnet-4-5-20250929, which is what I ran these examples against when this post went out in January 2026. Model generations have turned over since. Swap in whatever is current and re-check the routing behavior on your own tasks — the pattern is the point, not the model ID.

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 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)

    # 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 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.
# 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 factory name does not change the 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 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), 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"
    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"""),
    ])

    # 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 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 three phases have a simple contract: the planner creates executable calls, the worker fills their placeholders, and the solver receives the completed results. An unresolvable graph raises before the solver runs; a LangGraph retry policy or failure edge can then decide whether to re-plan. That contract is efficient only when the planner’s assumptions survive contact with the tools.

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)Accumulated message historyPinned 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:

    # 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_snapshot"),
        ToolMessage(tool_call_id="1", content="$132.45"),
        AIMessage(content="Now I need metrics..."),
        # ... agent continues until step complete
    ]

    In this full-history implementation, every model call re-reads a history that grows for the whole task. A production loop may summarize or truncate it. Plan-and-Execute runs ReAct loops too, but each one starts from that step’s description plus a digest of earlier findings, not the full tool-call history.

  3. ReWOO creates explicit, executable tool call specifications:

    # Planner output (list of ReWOOPlanStep objects)
    rewoo_plan = [
        ReWOOPlanStep(
            step_id="#E1",
            tool_name="get_stock_snapshot",
            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:

  • Plan-and-Execute: state moves through plancurrent_step_indexresearch_data.
  • ReAct: state accumulates in the messages array (the full conversation history).
  • 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 converge on the same acceptance check
    builder.add_edge("evaluator", "publish")
    builder.add_edge("publish", END)

    return builder.compile(
        checkpointer=checkpointer,
        # Human-in-the-loop pause: the reporter (or the ReWOO solver) writes a
        # draft, a fresh-context evaluator — a second model session with no
        # history of the run — votes on it, and the graph stops before publish.
        # The human approves a report that already carries an evaluator's
        # verdict rather than adjudicating raw research. The verdict is
        # advisory here — the graph routes to the interrupt either way.
        # Part 6 takes up how an acceptance check decides that a run is done.
        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, replay one task set through both routes and compare model calls, wall time, failures, and recovery behavior.

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