Engineering the Agentic Stack · Parte 1

Bucles de razonamiento de AI agents: ReAct, ReWOO y Plan-and-Execute

Traducción automática Este artículo se tradujo automáticamente a partir de la versión original en inglés.

Actualización del artículo

Publicado originalmente el 31 de enero de 2026. Revisado y actualizado el 6 de septiembre de 2026. La actualización se centra en las capacidades más recientes de los modelos y las reasoning APIs, con ejemplos y enlaces a fuentes revisados.

Un agent reasoning loop es el flujo de control que decide cuándo un modelo planifica, hace un tool call, lee el resultado y se detiene. Para un ingeniero que construye un agent, esa elección también es un presupuesto: determina con qué frecuencia se ejecuta el modelo, cuánto historial lleva cada llamada y si un resultado inesperado puede cambiar la siguiente acción.

En este artículo comparo ReAct, ReWOO y Plan-and-Execute mediante un LangGraph Market Analyst Agent que he construido. Te llevarás una regla de routing y varias formas de implementación que puedes adaptar, en lugar de tres nombres más para añadir a un diagrama.

El loop es la capa más interna de la serie. A su alrededor se encuentran la memoria, las herramientas, la seguridad, el runtime y las comprobaciones previas a declarar una tarea como terminada; no lo sustituyen.

Para una comparación breve de frameworks, consulta Best AI Agent Frameworks in 2026.

El reasoning loop decide qué hacer a continuación. No almacena el estado, ejecuta herramientas ni autoriza efectos secundarios.

El harness es el programa de control entre el modelo y la máquina. Ensambla prompts a partir del estado almacenado (Parte 2), define las acciones que el modelo puede nombrar (Parte 3), autoriza las llamadas (Parte 4) y comprueba las evidencias antes de declarar terminada una tarea (Parte 6). Son problemas de ingeniería independientes, pero un turno pasa por los cuatro.

El runtime (Parte 5) proporciona el registro de sesión, el sandbox, el almacén de checkpoints y las trazas que sobreviven a un único proceso worker.

Dónde encaja cada parte de la serie Engineering the Agentic StackDónde encaja cada parte de la serie Engineering the Agentic Stack

Cada artículo se puede leer de forma independiente. Juntos avanzan desde el loop hacia el exterior.


Empieza por el límite de fallo

Un buen prompt no resuelve la cuestión del flujo de control. Los patrones se diferencian por cuánto trabajo queda fijado antes del primer tool call. Eso determina el número de llamadas al modelo, cuándo se hace visible un plan incorrecto y si un resultado inesperado de una herramienta puede redirigir la ejecución.

Tres patrones de razonamiento para AI agents

Comparación de ReAct, ReWOO y Plan-and-Execute según el momento en que las evidencias pueden cambiar el planComparación de ReAct, ReWOO y Plan-and-Execute según el momento en que las evidencias pueden cambiar el plan

ReAct: decide después de cada observación

ReAct (Yao et al., 2022), abreviatura de Reason + Act, mantiene la siguiente decisión cerca de la observación más reciente:

El artículo original utiliza texto explícito de Thought, Action y Observation. Las tool APIs nativas modernas exponen las llamadas propuestas y los tool results; no es necesario mostrar un pensamiento. El interleaved thinking es una capacidad independiente del modelo/API. En el patrón de prompting histórico:

  1. Thought: el agent genera un «pensamiento» para descomponer el objetivo y planificar el siguiente paso.
  2. Action: basándose en ese pensamiento, hace un tool call.
  3. Observation: el agent lee el resultado, que actualiza su comprensión para el siguiente pensamiento.

ReAct lee cada tool result antes de elegir la siguiente acciónReAct lee cada tool result antes de elegir la siguiente acción

Esto proporciona a ReAct varias propiedades útiles:

  • En el ejemplo manual de PaLM-540B sobre HotpotQA del artículo, las observaciones de Wikipedia produjeron menos hechos alucinados que el prompting chain-of-thought.
  • El agent puede cambiar de estrategia sobre la marcha según lo que acaba de observar.
  • El historial de tool calls y observaciones proporciona una traza de ejecución concreta.

El mismo loop también tiene costes:

  • En una implementación ingenua con todo el historial y sin caching, cada turno vuelve a procesar el historial creciente. El prompt caching cambia el coste de entrada y el tiempo de procesamiento, pero no la ocupación de la context window ni las observaciones obsoletas. Mide por separado los tokens cached y uncached; la summarization y la truncation descartan contexto.
  • Es ineficiente cuando los tool calls podrían haberse planificado de antemano, que es precisamente el nicho que cubre ReWOO.
  • Sin una condición de parada o un límite de pasos, el loop puede ejecutarse indefinidamente.

Úsalo para tareas exploratorias, debugging y trabajos en los que no puedas predecir la siguiente acción.

ReWOO: compila primero el grafo de herramientas

ReWOO (Reasoning WithOut Observation) separa la planificación de la ejecución. El planner escribe la secuencia completa de herramientas en un único paso, utilizando placeholders para valores que solo existen después de la ejecución.

  1. Plan: una llamada al LLM escribe el plan completo de tool calls, utilizando placeholders de variables (#E1, #E2) para outputs que aún no existen.
  2. Worker: un ejecutor que no es un LLM ejecuta las herramientas planificadas y rellena los placeholders. El worker del artículo sigue el plan; la implementación posterior añade batches paralelos conscientes de las dependencias para los pasos listos.
  3. Solver: una llamada final al LLM recibe las observaciones recopiladas y redacta la respuesta.

ReWOO planifica el dependency graph antes de ejecutar las herramientasReWOO planifica el dependency graph antes de ejecutar las herramientas

Esta separación ofrece:

  • Menos llamadas repetidas al modelo que ReAct cuando el plan inicial sigue siendo válido.
  • Menos historial de prompts repetido que en un loop intercalado con todo el historial. La latencia de las herramientas sigue dependiendo de cómo el worker planifique las llamadas.
  • El planner puede someterse a fine-tuning por separado, sin un entorno activo.

También crea un límite estricto. En la prueba de estrés sobre HotpotQA del artículo, cada herramienta devolvía No evidence found; ReWOO perdió menos precisión que ReAct porque las observaciones fallidas no hicieron que su planner entrara en otro loop. Es robustez relativa, no una política de recuperación durante la ejecución. Una implementación aún debe decidir si un error de herramienta se convierte en evidencia para el solver, activa un reintento o aborta la ejecución. ReWOO encaja en workflows predecibles; no vuelve a planificar por sí solo alrededor de un grafo inicial incorrecto.

Úsalo para snapshots rápidos, comprobaciones de estado y dashboards cuyo comportamiento de las herramientas sea predecible.

Plan-and-Execute: descompón y después reacciona localmente

Plan-and-Solve prompting describe un método de prompting que primero crea un plan y después resuelve las subtareas. Un patrón relacionado de tool orchestration suele denominarse Plan-and-Execute. La guía de Plan-and-Execute de LangChain documenta ese patrón:

  1. Fase de planificación: el agent genera primero un plan que descompone la tarea en subtareas más pequeñas.
  2. Fase de ejecución: después lleva a cabo esas subtareas una a una. Cuando intervienen herramientas, cada subtarea suele ejecutarse como su propio pequeño loop ReAct, de modo que el executor puede seguir reaccionando a lo que devuelve una herramienta aunque el plan general sea fijo.

El artículo original se centraba en zero-shot prompting. En una implementación que utiliza herramientas, el patrón de orchestration puede ejecutar los pasos planificados secuencialmente y usar modelos distintos para la planificación y la ejecución. Esa separación de modelos es una decisión de implementación, no un resultado establecido por el artículo de Plan-and-Solve.

Variante opcional de replanning en Plan-and-Execute: cada paso utiliza feedback y un replanner puede revisar el plan generalVariante opcional de replanning en Plan-and-Execute: cada paso utiliza feedback y un replanner puede revisar el plan general

La figura incluye una rama opcional de replanning. El grafo didáctico posterior de este artículo no la utiliza: el feedback puede cambiar el trabajo dentro de un paso, pero no el plan restante.

El patrón resulta útil porque proporciona:

  • Razonamiento jerárquico que refleja cómo un experto humano descompone un proyecto.
  • Una arista explícita de replanning puede pausar la ejecución y reevaluar el plan después de un resultado inesperado.
  • Especialización de modelos. El planner puede ser caro y el executor, barato.
  • Con un checkpointer configurado, cada paso completado puede convertirse en un límite de reanudación.

Sus costes son:

  • Más round trips al modelo que ReWOO cuando cada paso contiene su propio loop ReAct.
  • Más estado que gestionar.
  • Es excesivo para consultas de un solo paso.

Úsalo para análisis complejos e investigación que necesiten una síntesis final.

Elige según dónde pueda fallar el plan

CaracterísticaReAct (2022)Plan-and-Execute (2023)ReWOO (2023)
Filosofía centralImprovisador: decide el siguiente movimiento a partir del último resultado, una llamada cada vez.Arquitecto: construye un blueprint completo, lo ejecuta y después lo revisa.Optimizador: compila un dependency graph y después agrupa las llamadas que están listas.
WorkflowLoop iterativo: Thought → Action → Observation.Dos fases: Fase 1 (Planning), Fase 2 (Execution).Desacoplado: el Planner escribe un grafo de tool calls; el Worker los ejecuta; el Solver compone la respuesta.
AdaptabilidadMáxima: puede cambiar de dirección después de cada tool call.Cada paso puede responder a su resultado. Un replanner opcional puede revisar los pasos posteriores.Mínima: el script del planner se ejecuta hasta el final; nada vuelve a planificar durante la ejecución.
EficienciaUn loop ingenuo con todo el historial repite más tokens de entrada; la gestión del contexto puede limitar ese crecimiento.El loop ReAct de cada paso puede empezar con un contexto corto en lugar del historial completo de la ejecución; un replanner añade llamadas de planificación cuando está habilitado.Menos llamadas al modelo; el worker de este artículo también agrupa las herramientas cuyas dependencias están listas.
Mejor paraExploración abierta o tareas cuyos resultados son impredecibles.Tareas de horizonte largo que requieren mantener un objetivo estable (por ejemplo, escribir un artículo).Workflows estructurados y repetibles (por ejemplo, consultar el tiempo en 5 ciudades).

La tabla sirve para orientar el routing, no como benchmark. Usa ReAct cuando un tool result pueda cambiar la siguiente acción. Usa Plan-and-Execute cuando la tarea se divida en pasos, pero cada paso siga necesitando feedback. Añade un replanner solo cuando un resultado deba cambiar los pasos posteriores; el grafo didáctico de abajo no tiene uno. Usa ReWOO cuando todas las dependencias de las herramientas se conozcan antes de la ejecución. Su dependency graph permite al worker didáctico ejecutar en paralelo las llamadas listas y detectar un grafo que no puede avanzar. Con el helper execute_tool del companion, los errores de herramientas capturados se convierten en strings que se pasan al solver. Una excepción que escape del helper aborta el worker. Ninguna de las dos rutas proporciona reintentos ni replanning. Mide los tres patrones con tu modelo, la latencia de las herramientas, el conjunto de tareas y la política de reintentos antes de optimizar el número de llamadas.

Qué cambia con los modelos y harnesses actuales

Estos patrones describen decisiones externas al modelo. Un modelo que razona entre tool calls sigue necesitando un programa que ejecute esas llamadas, detenga el loop y autorice los efectos. En Claude Sonnet 5, el adaptive thinking está habilitado por defecto y su texto se omite por defecto. Un bloque de thinking vacío no significa que el modelo haya omitido el razonamiento. Conserva los bloques de thinking completos y firmados al devolver tool results; reconstruir la conversación a partir del texto visible rompe esa continuidad. Compara el esfuerzo de razonamiento y los tokens de output facturados, además del número de llamadas al modelo.

Para una implementación nueva con LangChain, empieza por create_agent. Este ejemplo autocontenido utiliza el identificador actual de Sonnet y una herramienta fixture local. Configura ANTHROPIC_API_KEY e instala langchain y langchain-anthropic antes de invocarlo; esa invocación realiza peticiones de pago al modelo.

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)

Este es un loop de herramientas guiado por observaciones. Para esa única consulta no hace falta un planner independiente. He dejado fuera los overrides de sampling del ejemplo: una actualización del modelo también requiere comprobar los parámetros aceptados, los bloques de respuesta y el comportamiento de structured output. El ejemplo se comprobó offline para imports y construcción, pero no se evaluó con inferencia en vivo.

Si el trabajo necesita contextos aislados de subagents, archivos y gestión automática del contexto, Deep Agents empaqueta esas capacidades alrededor del mismo loop de herramientas. Es una decisión de harness, no un cuarto algoritmo de razonamiento. Compara un único agent con delegation en tareas que realmente se dividan en trabajo independiente; incluye el coste de coordinación y la pérdida de contexto en el resultado.

Ejemplo completo: el Market Analyst Agent

El Market Analyst Agent hace concreta la diferencia. Un único codebase utiliza los tres patrones para la investigación de mercado, y un router elige entre una ruta de investigación profunda y otra de flash briefing. Los fragmentos siguientes son variantes didácticas abreviadas de commit b4e769a: el worker de abajo añade batches paralelos de dependencias listas y lanza una excepción cuando el grafo no puede avanzar. Su execute_tool es el helper companion, que captura excepciones de herramientas y devuelve strings de error al solver. Solo las excepciones que escapan de ese helper abortan una ejecución futura. Esta adaptación didáctica no añade reintentos ni recuperación.

Utiliza LangGraph para la orchestration. Un node es una función de Python que devuelve campos que actualizar en el estado compartido. Una edge declara el siguiente node y puede invocar una función de routing. LangGraph fusiona las actualizaciones y crea checkpoints en los límites de super-step: un node o un batch de nodes paralelos. Las pending writes conservan los resultados correctos de los siblings cuando falla otro node. Los tres patrones comparten un único objeto de estado, por lo que el routing no requiere tres schemas independientes:

El Market Analyst Agent dirige una petición a dos reasoning loops con estado compartidoEl Market Analyst Agent dirige una petición a dos reasoning loops con estado compartido

El diagrama aísla el routing y la creación del borrador. Omite el evaluator compartido y la aprobación humana antes de publicar, que se muestran más adelante, para que los dos reasoning loops sigan siendo legibles.

Definición del estado

Los bloques de Python siguientes son fragmentos de integración, no scripts independientes: comparten tipos de estado, helpers de nodes e imports del framework del companion. El runner de ejemplos aislados del repositorio los omite; las comprobaciones de contrato offline cubren el estado, las llamadas propuestas, los IDs de mensajes y el comportamiento de reanudación.

El schema de estado contiene los campos que necesitan ambos modos:

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)

Patrón 1: implementación de Plan-and-Execute

Plan-and-Execute encaja con la síntesis de varios pasos. Un planner escribe los pasos de alto nivel y después un loop ReAct ejecuta cada paso y reacciona a los tool results.

Los siguientes fragmentos históricos del companion conservan claude-sonnet-4-5-20250929 y la API anterior create_react_agent para seguir siendo comparables con el commit enlazado. El punto de partida actual es el ejemplo create_agent de arriba. Migrar el grafo completo requiere probar conjuntamente sus schemas del planner, el manejo de mensajes internos, el routing y el comportamiento de reanudación; cambiar únicamente el string del modelo no constituye esa migración.

La implementación mantiene visibles cuatro límites:

  1. Una única fase inicial de planificación. Una sola llamada al LLM produce el plan completo como una lista de descripciones de pasos.
  2. Output guiado por schema, que valida la forma de la respuesta. La ejecución necesita una comprobación separada del plan.
  3. Todavía no se ejecutan herramientas. El planner solo decide qué hacer, no cómo.
  4. Pasos legibles para humanos. Cada paso es texto que interpretará un executor.
# 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
    }

Esa línea llm.with_structured_output(PlanOutput) es Schema-Guided Reasoning (SGR), que traté en un artículo anterior. El schema rechaza campos mal formados. Antes de ejecutar, exige también una lista de pasos no vacía y acotada, con números de paso únicos y descripciones utilizables; este schema abreviado no impone esas condiciones. El companion aún puede aceptar un plan vacío y fallar cuando su executor indexe el primer paso.

Patrón 2: ejecución con ReAct

Una vez creado el plan, el executor ejecuta cada paso como su propio loop ReAct. Esta es la Fase 2: cada paso es lo bastante pequeño como para que un ciclo Thought-Action-Observation se mantenga centrado, y el agent puede reaccionar a lo que devuelva la herramienta.

Así encaja la parte de ReAct:

  1. Ejecución iterativa. Un paso cada vez, con feedback de las observaciones.
  2. El loop entre el modelo y los tool results se ejecuta dentro de create_react_agent; la factory no garantiza que exista un transcript de razonamiento expuesto.
  3. Los resultados de los pasos anteriores se incorporan como contexto para el razonamiento actual.
  4. El agent elige las herramientas basándose en la descripción del paso.
  5. Puede cambiar de enfoque durante el paso según lo que devuelva una herramienta.
# 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,
    }

Patrón 3: ReWOO para snapshots rápidos

Para un briefing rápido, ReWOO elimina las llamadas al modelo de la fase de ejecución. Las herramientas independientes se ejecutan en paralelo; las dependientes esperan a sus prerrequisitos. El planner emite el grafo de herramientas de antemano y el worker lo ejecuta sin preguntar al modelo qué debe hacer a continuación.

Su estructura es la siguiente:

  1. Tres fases (Planner → Worker → Solver). El worker no pregunta al modelo qué debe hacer a continuación ni vuelve a planificar.
  2. Los tool calls hacen referencia a los placeholders #E1 y #E2 para resultados que aún no existen.
  3. No hay ningún LLM durante la ejecución. El worker simplemente ejecuta las herramientas.
  4. Las herramientas independientes se ejecutan en paralelo.
  5. Una única llamada de síntesis al final, sobre todos los datos a la vez.

Fase 1: planner de ReWOO (escribe de antemano un plan tipado de llamadas propuestas)

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}

Fase 2: worker de ReWOO (ejecuta las herramientas sin razonamiento del LLM)

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

Fase 3: solver de ReWOO (sintetiza todos los resultados en una única llamada al LLM)

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

El planner escribe las llamadas propuestas, el worker rellena sus placeholders y el solver recibe los resultados. El schema solo comprueba la forma de la respuesta. Antes del dispatch, rechaza planes vacíos o demasiado grandes, IDs duplicados, dependencias inexistentes y ciclos. Los IDs duplicados se colapsan silenciosamente en el diccionario pending anterior. Comprueba los nombres y argumentos de las herramientas frente a un registry, inspecciona recursivamente los placeholders dentro de listas y objetos anidados, contrástalos con depends_on y rechaza las referencias sin resolver antes de ejecutar la llamada afectada. Este worker didáctico omite esas comprobaciones y envía los pasos listos a execute_tool del companion, incluido su comportamiento de error como evidencia. El solver debe tratar un string de error como evidencia ausente, no como una consulta correcta. Si el grafo no puede avanzar, se detiene antes del solver; no existe una ruta de reintento ni de replanning. Reintentar el mismo estado solo repite el mismo plan, por lo que el replanning necesita un resultado de fallo, una edge condicional y un límite de intentos.

Dónde llama cada patrón al modelo

PatrónLlamadas al LLM durante la ejecuciónActualizaciones de estadoPatrón de código clave
Plan-and-Execute1 para la planificación + un loop ReAct por paso (varias llamadas cada uno) + 1 para el informeFinalización secuencial de pasosplanner_node() → loop: executor_node()reporter_node()
ReAct (dentro de cada paso)Varias por paso (ciclos thought-action)Solo transcript interno; el grafo externo registra los resultados de los pasos completadosEl companion fijado utiliza create_react_agent(), ya obsoleto
ReWOO1 para la planificación + 0 durante la ejecución + 1 para la síntesisBatches de herramientas conscientes de las dependenciasrewoo_planner_node()rewoo_worker_node()rewoo_solver_node()

La diferencia importante está en el output del planner. Determina cuánta libertad conserva el executor:

  1. Plan-and-Execute crea descripciones de pasos legibles para humanos:

    # 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
    ]

    El executor lee cada descripción y decide qué herramientas llamar. Es flexible, pero cada paso es su propio loop ReAct, por lo que un paso cuesta varias llamadas al modelo, no una.

  2. ReAct no tiene un plan inicial. Utiliza razonamiento iterativo:

    # 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
    ]

    En un loop ReAct independiente con todo el historial, cada llamada al modelo lleva un historial que crece durante toda la tarea. Los cache hits pueden reducir el cálculo repetido y los cargos de entrada; un loop de producción también puede resumirlo o truncarlo. El ejemplo de Plan-and-Execute inicia cada invocación ReAct con el paso actual y un digest de los hallazgos anteriores. Conserva el resultado completado en plan, no el transcript interno de tool calls.

  3. ReWOO crea tool calls explícitos y tipados:

    # 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
    ]

    El worker trabaja a ciegas, sin intervención del LLM. Todas las llamadas al modelo están en el planner y el solver, lo que hace predecible su número.

Flujo de memoria y estado:

  • Plan-and-Execute: el estado pasa por plancurrent_step_indexresearch_data.
  • ReAct dentro de este grafo de Plan-and-Execute: el agent interno produce el transcript de un paso. El grafo externo conserva plan, current_step_index y los resultados de los pasos completados.
  • ReWOO: el estado pasa por rewoo_plan, cuyos campos result rellena el worker.

Conecta ambas rutas en un único grafo

El grafo tiene dos rutas visibles para el usuario sobre un único AgentState: la investigación profunda utiliza Plan-and-Execute con un loop ReAct dentro de cada paso, mientras que el flash briefing utiliza ReWOO. ReAct es aquí una primitiva de ejecución, no una tercera ruta.

Esta implementación no tiene replanner: ejecuta el plan inicial hasta el final. Añadir replanning requeriría una edge desde executor de vuelta a planner y una regla para decidir cuándo un resultado inesperado justifica otra llamada al modelo.

LangGraph mantiene el wiring de forma declarativa:

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

Selección automática del patrón mediante un router

El router asigna la forma de la petición a una ruta. Schema-Guided Reasoning limita el output del 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)

Con este router, «current price» se dirige a ReWOO y «investment thesis» a Plan-and-Execute. El valor predeterminado es la investigación profunda cuando la petición es ambigua. Antes de poner el router delante de los usuarios, compara ambas rutas con un workflow fijo y las mismas tareas, herramientas, evidencias y presupuesto. Repite pruebas estocásticas y cuenta cada intento iniciado. Registra el éxito de la tarea, la aceptación incorrecta, los tokens cached y uncached, el tiempo de pared y la recuperación tras errores de herramientas u observaciones engañosas. Estos patrones son decisiones de flujo de control, no un ranking de adopción; el relato de ingeniería de Anthropic también recomienda empezar con workflows simples y componibles.

La implementación completa del companion, incluido el router y el estado compartido, está en el commit fijado de Market Analyst Agent.

La siguiente capa es la memoria

La Parte 2, AI Agent Memory Architecture, separa los checkpoints reanudables del conocimiento entre sesiones y los documentos del proyecto. Sin esa capa de estado, el router y el executor anteriores solo funcionan mientras un proceso y una context window permanezcan activos.

Referencias