Engineering the Agentic Stack · Deel 1

Reasoning loops van AI agents: ReAct, ReWOO en Plan-and-Execute

Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

Artikelupdate

Oorspronkelijk gepubliceerd op 31 januari 2026. Gereviewd en bijgewerkt op 6 september 2026. De update richt zich op nieuwere modelmogelijkheden en reasoning APIs, met herziene voorbeelden en bronlinks.

Een agent reasoning loop is de control flow die bepaalt wanneer een model plant, een tool aanroept, het resultaat leest en stopt. Voor een engineer die een agent bouwt, is die keuze ook een budget: ze bepaalt hoe vaak het model draait, hoeveel history elke call meedraagt en of een onverwacht resultaat de volgende actie kan veranderen.

Dit artikel vergelijkt ReAct, ReWOO en Plan-and-Execute aan de hand van een LangGraph Market Analyst Agent die ik heb gebouwd. Je houdt er een routingregel en aanpasbare implementatievormen aan over, in plaats van drie namen om aan een diagram toe te voegen.

De loop is de binnenste laag van de serie. Memory, tools, security, runtime en checks voordat een task als voltooid wordt gemarkeerd, omringen deze laag; ze vervangen haar niet.

Zie Best AI Agent Frameworks in 2026 voor de korte frameworkvergelijking.

De reasoning loop bepaalt wat er vervolgens gebeurt. Hij slaat geen state op, voert geen tools uit en autoriseert geen side effects.

De harness is het control program tussen het model en de machine. Deze stelt prompts samen uit opgeslagen state (Part 2), definieert de acties die het model mag benoemen (Part 3), autoriseert calls (Part 4) en controleert het bewijsmateriaal voordat een task als voltooid wordt gemarkeerd (Part 6). Dit zijn afzonderlijke engineeringproblemen, maar één turn doorloopt ze alle vier.

De runtime (Part 5) levert de session log, sandbox, checkpoint store en traces die langer meegaan dan één worker process.

Waar elk onderdeel van de serie Engineering the Agentic Stack zich bevindtWaar elk onderdeel van de serie Engineering the Agentic Stack zich bevindt

Elke post staat op zichzelf. Samen bewegen ze van de loop naar buiten.


Begin bij de failure boundary

Een goede prompt beantwoordt de vraag over control flow niet. De patronen verschillen in hoeveel werk vóór de eerste tool call wordt vastgelegd. Dat bepaalt het aantal model calls, wanneer een slecht plan zichtbaar wordt en of een onverwacht tool result de run kan ombuigen.

Drie reasoning patterns voor AI agents

ReAct, ReWOO en Plan-and-Execute vergeleken op basis van het moment waarop evidence het plan mag veranderenReAct, ReWOO en Plan-and-Execute vergeleken op basis van het moment waarop evidence het plan mag veranderen

ReAct: beslis na elke observation

ReAct (Yao et al., 2022), een afkorting van Reason + Act, houdt de volgende beslissing dicht bij de meest recente observation:

Het oorspronkelijke paper gebruikt expliciete tekst voor Thought, Action en Observation. Moderne native tool APIs maken voorgestelde calls en tool results beschikbaar; een zichtbare thought is niet vereist. Interleaved thinking is een afzonderlijke model/API-capability. In het historische prompting pattern:

  1. Thought: de agent genereert een “thought” om het doel op te splitsen en de volgende stap te plannen.
  2. Action: op basis van de thought roept de agent een tool aan.
  3. Observation: de agent leest het resultaat, waardoor zijn begrip voor de volgende thought wordt bijgewerkt.

ReAct leest elk tool result voordat de volgende action wordt gekozenReAct leest elk tool result voordat de volgende action wordt gekozen

Hierdoor heeft ReAct nuttige eigenschappen:

  • In het handmatige PaLM-540B HotpotQA-voorbeeld uit het paper produceerden Wikipedia-observations minder gehallucineerde feiten dan chain-of-thought prompting.
  • De agent kan zijn strategie on the fly aanpassen op basis van wat hij zojuist heeft gezien.
  • De history van tool calls en observations levert een concrete execution trace op.

Aan dezelfde loop zijn ook kosten verbonden:

  • In een naïeve full-history-implementatie zonder caching verwerkt elke turn de steeds langer wordende history opnieuw. Prompt caching verandert de inputkosten en verwerkingstijd, maar niet de bezetting van de context window of stale observations. Meet cached en uncached tokens afzonderlijk; summarization en truncation verwijderen context.
  • Verspillend wanneer de tool calls vooraf konden worden gepland; dat is precies de niche van ReWOO.
  • Zonder stopconditie of step limit kan de loop oneindig blijven draaien.

Gebruik ReAct voor exploratieve tasks, debugging en werk waarbij je de volgende action niet kunt voorspellen.

ReWOO: compileer eerst de tool graph

ReWOO (Reasoning WithOut Observation) scheidt planning van execution. De planner schrijft de volledige tool sequence in één pass, met placeholders voor waarden die pas na execution bestaan.

  1. Plan: één LLM call schrijft het volledige plan van tool calls, met variabele placeholders (#E1, #E2) voor outputs die nog niet bestaan.
  2. Worker: een non-LLM executor voert de geplande tools uit en vult de placeholders in. De worker van het paper volgt het plan; de implementatie verderop in dit artikel voegt dependency-aware parallel batches toe voor steps die gereed zijn.
  3. Solver: een laatste LLM call ontvangt de verzamelde observations en schrijft het antwoord.

ReWOO plant de dependency graph voordat tools worden uitgevoerdReWOO plant de dependency graph voordat tools worden uitgevoerd

Deze scheiding biedt:

  • Minder herhaalde model calls dan ReAct wanneer het initiële plan geldig blijft.
  • Minder herhaalde prompt history dan een interleaved full-history-loop. Tool latency hangt nog steeds af van de manier waarop de worker calls plant.
  • De planner kan afzonderlijk worden fine-getuned, zonder live environment.

Er ontstaat ook een harde grens. In de HotpotQA-stresstest van het paper gaf elke tool No evidence found terug; ReWOO verloor minder accuracy dan ReAct omdat de mislukte observations de planner niet naar een nieuwe loop stuurden. Dat is relatieve robustness, geen execution recovery policy. Een implementatie moet nog steeds bepalen of een tool error solver evidence wordt, een retry triggert of de run afbreekt. ReWOO past bij voorspelbare workflows; het re-plant niet zelfstandig rond een slechte initiële graph.

Gebruik het voor snelle snapshots, statuschecks en dashboards waarvan het toolgedrag voorspelbaar is.

Plan-and-Execute: decompositie, daarna lokaal reageren

Plan-and-Solve prompting beschrijft een prompting-methode die eerst een plan maakt en daarna de subtasks oplost. Een verwant tool-orchestration pattern wordt doorgaans Plan-and-Execute genoemd. De Plan-and-Execute guide van LangChain documenteert dit pattern:

  1. Planning phase: de agent genereert eerst een plan dat de task opsplitst in kleinere subtasks.
  2. Execution phase: de agent voert deze subtasks vervolgens één voor één uit. Zodra tools betrokken zijn, draait elke subtask doorgaans als een eigen kleine ReAct-loop, zodat de executor nog steeds kan reageren op wat een tool teruggeeft, ook al ligt het algemene plan vast.

Het oorspronkelijke paper richtte zich op zero-shot prompting. In een tool-using implementatie kan het orchestration pattern de geplande steps sequentieel uitvoeren en verschillende models gebruiken voor planning en execution. Die model split is een implementatiekeuze, geen resultaat dat door het Plan-and-Solve-paper is vastgesteld.

Optionele Plan-and-Execute-replanningvariant: elke step gebruikt feedback en een replanner kan het algemene plan herzienOptionele Plan-and-Execute-replanningvariant: elke step gebruikt feedback en een replanner kan het algemene plan herzien

De figuur bevat een optionele replanning branch. De teaching graph verderop in dit artikel gebruikt deze niet: feedback kan het werk binnen één step veranderen, maar niet het resterende plan.

Het pattern is nuttig omdat het het volgende biedt:

  • Hierarchical reasoning die weerspiegelt hoe een menselijke expert een project opsplitst.
  • Een expliciete replanning edge kan pauzeren en na een onverwacht step result opnieuw beoordelen.
  • Model specialization. De planner kan duur zijn en de executor goedkoop.
  • Met een geconfigureerde checkpointer kan elke voltooide step een resume boundary worden.

De kosten zijn:

  • Meer model round trips dan ReWOO wanneer elke step zijn eigen ReAct-loop bevat.
  • Meer state om te beheren.
  • Overkill voor one-shot queries.

Gebruik het voor complexe analysis en research die een finale synthesis nodig hebben.

Kies op basis van waar het plan kan falen

FeatureReAct (2022)Plan-and-Execute (2023)ReWOO (2023)
Core philosophyImproviser: bepaal de volgende stap op basis van het laatste resultaat, één call per keer.Architect: bouw een volledig blueprint, voer het uit en review het daarna.Optimizer: compileer een dependency graph en batch vervolgens de calls die gereed zijn.
WorkflowIteratieve loop: Thought → Action → Observation.Two-stage: Phase 1 (Planning), Phase 2 (Execution).Ontkoppeld: Planner schrijft een graph van tool calls; Worker voert ze uit; Solver stelt het antwoord samen.
AdaptabilityHoogste: kan na elke afzonderlijke tool call van richting veranderen.Elke step kan op het resultaat reageren. Een optionele replanner kan latere steps herzien.Laagste: het script van de planner wordt volledig uitgevoerd; tijdens de run wordt niets opnieuw gepland.
EfficiencyEen naïeve full-history-loop herhaalt meer input tokens; context management kan deze groei beperken.De ReAct-loop van elke step kan met een korte context starten in plaats van met de history van de hele run; een replanner voegt planning calls toe wanneer die is ingeschakeld.Minder model calls; de worker in dit artikel batcht ook dependency-ready tools.
Best forOpen-ended exploratie of tasks met onvoorspelbare resultaten.Long-horizon tasks die een stabiel doel vereisen (bijv. een paper schrijven).Structured, repeatable workflows (bijv. het weer in 5 steden controleren).

De tabel helpt bij routing, maar is geen benchmark. Gebruik ReAct wanneer een tool result de volgende action kan veranderen. Gebruik Plan-and-Execute wanneer de task in steps uiteenvalt, maar elke step nog feedback nodig heeft. Voeg alleen een replanner toe wanneer één resultaat latere steps moet veranderen; de teaching graph hieronder heeft er geen. Gebruik ReWOO wanneer elke tool dependency vóór execution bekend is. Dankzij de dependency graph kan de teaching worker ready calls parallel uitvoeren en een graph detecteren die niet verder kan. Met de execute_tool-helper van de companion worden opgevangen tool errors strings die aan de solver worden doorgegeven. Een exception die uit de helper ontsnapt, breekt de worker af. Geen van beide paden levert retries of replanning. Meet alle drie met jouw model, tool latency, task set en retry policy voordat je optimaliseert voor het aantal calls.

Wat verandert met huidige models en harnesses

Deze patterns beschrijven beslissingen buiten het model. Een model dat tussen tool calls door redeneert, heeft nog steeds een program nodig om die calls uit te voeren, de loop te stoppen en effects te autoriseren. Bij Claude Sonnet 5 is adaptive thinking standaard ingeschakeld en wordt de tekst ervan standaard weggelaten. Een leeg thinking block betekent niet dat het model reasoning heeft overgeslagen. Bewaar volledige ondertekende thinking blocks wanneer je tool results terugstuurt; de conversatie opnieuw opbouwen uit alleen zichtbare tekst verbreekt die continuïteit. Vergelijk reasoning effort en gefactureerde output tokens, naast het aantal model calls.

Begin voor een nieuwe LangChain-implementatie met create_agent. Dit self-contained wiring-voorbeeld gebruikt de huidige Sonnet-identifier en een lokale fixture tool. Stel ANTHROPIC_API_KEY in en installeer langchain plus langchain-anthropic voordat je het aanroept; die invocation maakt betaalde 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)

Dit is een observation-driven tool loop. Voor die ene lookup is geen afzonderlijke planner nodig. Ik laat sampling overrides uit het voorbeeld: een model upgrade vereist ook controle van geaccepteerde parameters, response blocks en structured-outputgedrag. Het voorbeeld is offline gecontroleerd op imports en construction, maar niet geëvalueerd met live inference.

Als de job geïsoleerde subagent contexts, files en automatic context management nodig heeft, verpakt Deep Agents deze capabilities rond dezelfde tool loop. Dat is een harness-keuze, geen vierde reasoning algorithm. Vergelijk een single agent met delegation op tasks die daadwerkelijk in onafhankelijk werk kunnen worden opgesplitst; neem coordination cost en verloren context mee in het resultaat.

Een uitgewerkt voorbeeld: de Market Analyst Agent

De Market Analyst Agent maakt het onderscheid concreet. Eén codebase gebruikt alle drie de patterns voor market research, en een router kiest tussen een deep-research path en een flash-briefing path. De onderstaande excerpts zijn ingekorte teaching variants van commit b4e769a: de worker hieronder voegt dependency-ready parallel batches toe en geeft een exception wanneer de graph niet verder kan. De execute_tool ervan is de companion helper, die tool exceptions opvangt en error strings voor de solver teruggeeft. Alleen exceptions die uit die helper ontsnappen, breken een future af. Deze teaching adaptation voegt geen retries of recovery toe.

Hij gebruikt LangGraph voor orchestration. Een node is een Python function die fields retourneert om in shared state bij te werken. Een edge declareert de volgende node en kan een routing function aanroepen. LangGraph merge’t updates en maakt checkpoints op super-step boundaries: één node of een batch van parallelle nodes. Pending writes bewaren succesvolle resultaten van sibling nodes wanneer een andere node faalt. De drie patterns delen één state object, zodat routing geen drie afzonderlijke schemas vereist:

Market Analyst Agent routeert één request naar twee reasoning loops met shared stateMarket Analyst Agent routeert één request naar twee reasoning loops met shared state

Het diagram isoleert routing en draft creation. De gedeelde evaluator en de human approval vóór publishing, die later worden getoond, zijn weggelaten zodat de twee reasoning loops leesbaar blijven.

State definition

De onderstaande Python-blokken zijn integration excerpts, geen standalone scripts: ze delen state types, node helpers en framework imports uit de companion. De geïsoleerde example runner van de repository slaat ze over; offline contract checks dekken de state, proposed calls, message IDs en resume behavior.

Het state schema bevat de fields die beide modes nodig hebben:

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

Plan-and-Execute past bij multi-step synthesis. Een planner schrijft de high-level steps, waarna een ReAct-loop elke step uitvoert en reageert op tool results.

De volgende historische companion excerpts behouden claude-sonnet-4-5-20250929 en de oudere create_react_agent API zodat ze vergelijkbaar blijven met de gelinkte commit. Het huidige startpunt is het create_agent-voorbeeld hierboven. Voor migratie van de volledige graph moet je de planner schemas, inner message handling, routing en resume behavior samen testen; alleen de model string wijzigen is geen dergelijke migratie.

De implementatie houdt vier boundaries zichtbaar:

  1. Eén voorafgaande planning phase. Eén LLM call produceert het volledige plan als een lijst step descriptions.
  2. Schema-guided output, die de response shape valideert. Execution vereist een afzonderlijke plan check.
  3. Nog geen tool execution. De planner bepaalt alleen wat er moet gebeuren, niet hoe.
  4. Human-readable steps. Elke step is tekst die een executor interpreteert.
# 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
    }

Die llm.with_structured_output(PlanOutput)-regel is Schema-Guided Reasoning (SGR), waarover ik schreef in een eerdere post. Het schema weigert malformed fields. Vereis vóór execution ook een niet-lege, begrensde step list met unieke step numbers en bruikbare descriptions; dit abridged schema dwingt die voorwaarden niet af. De companion kan nog steeds een leeg plan accepteren en falen wanneer zijn executor de eerste step indexeert.

Pattern 2: ReAct execution

Zodra het plan bestaat, voert de executor elke step uit als een eigen ReAct-loop. Dit is Phase 2: elke step is klein genoeg om een Thought-Action-Observation-cycle gericht te houden, en de agent kan reageren op wat de tool ook teruggeeft.

Zo sluit het ReAct-gedeelte aan:

  1. Iterative execution. Eén step per keer, met observation feedback.
  2. De model/tool-result-loop draait binnen create_react_agent; de factory belooft geen exposed reasoning transcript.
  3. Results van eerdere steps worden als context voor de huidige reasoning meegegeven.
  4. De agent kiest tools op basis van de step description.
  5. De agent kan zijn aanpak halverwege een step veranderen op basis van wat een tool teruggeeft.
# 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 voor snelle snapshots

Voor een korte briefing verwijdert ReWOO model calls uit de execution phase. Onafhankelijke tools draaien parallel; afhankelijke tools wachten op hun prerequisites. De planner emitteert de tool graph vooraf en de worker voert die uit zonder het model te vragen wat er vervolgens moet gebeuren.

De vorm:

  1. Drie phases (Planner → Worker → Solver). De worker vraagt het model niet wat er vervolgens moet gebeuren en plant niet opnieuw.
  2. Tool calls verwijzen naar #E1, #E2-placeholders voor resultaten die nog niet bestaan.
  3. Geen LLM tijdens execution. De worker voert alleen tools uit.
  4. Onafhankelijke tools draaien parallel.
  5. Eén synthesis call aan het einde, over alle data tegelijk.

Phase 1: ReWOO planner (schrijft vooraf een typed proposed-call plan)

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 (voert tools uit zonder 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 alle resultaten in één 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())}

De planner schrijft proposed calls, de worker vult hun placeholders in en de solver ontvangt de resultaten. Het schema controleert alleen de response shape. Weiger vóór dispatch lege of te grote plans, duplicate IDs, ontbrekende dependencies en cycles. Duplicate IDs worden in het bovenstaande pending-dictionary stilzwijgend samengevoegd. Controleer tool names en arguments tegen een registry, inspecteer placeholders recursief in geneste lists en objects, cross-check ze tegen depends_on en weiger unresolved references vóór de betreffende call. Deze teaching worker slaat die checks over en stuurt ready steps naar execute_tool van de companion, inclusief diens error-as-evidence-gedrag. De solver moet een error string behandelen als ontbrekende evidence, niet als een geslaagde lookup. Als de graph niet verder kan, stopt hij vóór de solver; er is geen retry- of replanning path. Dezelfde state retrien herhaalt alleen hetzelfde plan, dus replanning vereist een failure result, een conditional edge en een limiet op het aantal pogingen.

Waar elk pattern het model aanroept

PatternLLM calls tijdens executionState updatesKey code pattern
Plan-and-Execute1 voor planning + een ReAct-loop per step (meerdere calls elk) + 1 voor het reportSequentiële voltooiing van stepsplanner_node() → loop: executor_node()reporter_node()
ReAct (binnen elke step)Meerdere per step (thought-action-cycles)Alleen inner transcript; outer graph legt resultaten van voltooide steps vastPinned companion gebruikt deprecated create_react_agent()
ReWOO1 voor planning + 0 tijdens execution + 1 voor synthesisDependency-aware tool batchesrewoo_planner_node()rewoo_worker_node()rewoo_solver_node()

Het belangrijke verschil zit in de output van de planner. Die bepaalt hoeveel discretion de executor behoudt:

  1. Plan-and-Execute maakt 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
    ]

    De executor leest elke description en beslist welke tools moeten worden aangeroepen. Flexibel, maar elke step is een eigen ReAct-loop, dus een step kost meerdere model calls, niet één.

  2. ReAct heeft geen voorafgaand plan. Het gebruikt 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 een standalone full-history-ReAct-loop draagt elke model call history mee die voor de hele task groeit. Cache hits kunnen herhaalde computation en input charges verminderen; een production loop kan de history ook samenvatten of truncaten. Het Plan-and-Execute-voorbeeld start elke ReAct-invocation met de huidige step en een digest van eerdere findings. Het bewaart het voltooide resultaat in plan, niet het inner tool-call transcript.

  3. ReWOO maakt expliciete, 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
    ]

    De worker werkt blind, zonder LLM-betrokkenheid. Alle model calls bevinden zich in de planner en solver, waardoor het aantal model calls voorspelbaar is.

Memory en state flow:

  • Plan-and-Execute: state beweegt door plancurrent_step_indexresearch_data.
  • ReAct binnen deze Plan-and-Execute-graph: de inner agent produceert het transcript van één step. De outer graph bewaart plan, current_step_index en de resultaten van voltooide steps.
  • ReWOO: state beweegt door rewoo_plan, waarbij de worker de velden result invult.

Beide routes in één graph verbinden

De graph heeft twee user-facing routes over één AgentState: deep research gebruikt Plan-and-Execute met een ReAct-loop binnen elke step, terwijl flash briefing ReWOO gebruikt. ReAct is hier een execution primitive, geen derde route.

Deze implementatie heeft geen replanner: hij voert het initiële plan volledig uit. Voor replanning is een edge nodig van executor terug naar planner en een regel voor wanneer een verrassend resultaat een nieuwe model call rechtvaardigt.

LangGraph houdt de wiring declaratief:

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

Automatische patternselectie met een router

De router koppelt de requestvorm aan een route. Schema-Guided Reasoning beperkt de output van de classifier:

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)

Met deze router gaat “current price” naar ReWOO en “investment thesis” naar Plan-and-Execute. De default is deep research wanneer de request ambigu is. Vergelijk beide routes met een vaste workflow op dezelfde tasks, tools, evidence en budget voordat je de router voor gebruikers inzet. Herhaal stochastische trials en tel elke gestarte poging. Houd task success, incorrect acceptance, cached en uncached tokens, wall time en recovery na tool errors of misleidende observations bij. Deze patterns zijn control-flow-keuzes, geen adoption ranking; Anthropic’s engineering account raadt eveneens aan te beginnen met eenvoudige composable workflows.

De volledige companion-implementatie, inclusief router en shared state, staat in de gepinde Market Analyst Agent-commit.

De volgende laag is memory

Part 2, AI Agent Memory Architecture, maakt onderscheid tussen resumable checkpoints, kennis over meerdere sessions en projectdocumenten. Zonder die state layer werken de router en executor hierboven alleen zolang één process en één context window actief blijven.

Referenties