/pattern/rewoo/

01 · Single AgentReasoning without ObservationPlanner-Solver Pattern

ReWOO.
Plan. Batch. Fold.

The agent plans all necessary tool calls upfront, executes them in batch, and uses the aggregated results for the final answer.

When to reach for it

  • The required tool calls are identifiable at the beginning.
  • LLM calls need to be drastically reduced.

When it backfires

  • Tool results branch heavily.
  • Interactive error handling per intermediate step is critical.

The tradeoff

Significantly lower LLM costs and latency, but at the expense of adaptability during execution.

The mental model

A shape you can draw on a napkin.

Plan up front, run every tool in one batch, fold the results. No per-step observation; cheaper than ReAct, less adaptive.

PlannerTool 1Tool 2Tool 3Solver
Walk it through

A real run, step by step.

Task"Estimate the carbon footprint of a five-city European trip."
1 / 6
Planplanner(task) → [flight_lookup, train_lookup, hotel_lookup, carbon_coefficient]
BatchAll four tools run in parallel: flight_lookup(cities), train_lookup(cities), hotel_lookup(cities), carbon_coefficient(mode='air')
CollectResults: { flights: [...], trains: [...], hotels: [...], kg_per_km: 0.255 }
Solvesolver(results) → { total_kg: 1847, breakdown: [...], recommendations: [...] }
Respondrespond(solver_output) → Markdown estimate
AnswerMarkdown estimate with total kg and per-leg breakdown. Total: 1 plan + 4 parallel tool calls + 1 solve.
In code

The loop is already built in.

LangGraphpython
from typing import TypedDict, Any
from langgraph.graph import StateGraph, END

class Plan(TypedDict):
    tools: list[str]
    args: list[dict]

class State(TypedDict):
    task: str
    plan: Plan | None
    tool_outputs: dict[str, Any]
    answer: str | None

def planner(state):
    return {"plan": plan_llm(state["task"])}

# Parallel tool nodes via fan-out
def flight_lookup(state):
    return {"tool_outputs": {"flights": search_flights(state["plan"]["args"][0])}}

def train_lookup(state): ...
def hotel_lookup(state): ...
def carbon_lookup(state): ...

def solver(state):
    return {"answer": solve_llm(state["tool_outputs"])}

g = StateGraph(State)
g.add_node("planner", planner)
g.add_node("flight", flight_lookup)
g.add_node("train", train_lookup)
g.add_node("hotel", hotel_lookup)
g.add_node("carbon", carbon_lookup)
g.add_node("solver", solver)

# Fan-out from planner to all tools
g.add_edge("planner", "flight")
g.add_edge("planner", "train")
g.add_edge("planner", "hotel")
g.add_edge("planner", "carbon")
for node in ["flight", "train", "hotel", "carbon"]:
    g.add_edge(node, "solver")
g.add_edge("solver", END)
g.set_entry_point("planner")
rewoo = g.compile()
Pitfalls

Two ways this pattern will hurt you.

Plan was wrong but no observation step catches it

The planner assumed a direct flight exists. The flight tool returns an empty result, but there's no observation edge to replan — the solver folds incorrect data into the answer.

Fix · Add validation nodes after each tool, or wrap the batch in an evaluator-optimizer loop that can request a re-plan.

Tool failure has no recovery edge

One tool in the batch times out. The solver receives partial data and produces a misleading estimate.

Fix · Use a results aggregator that flags missing keys, or add a retry orchestration layer before the solver.

Framework support

Where ReWOO is native.

Search

Search patterns, frameworks, and pages.