Skip to Content

ReWOO

from loomflow.architecture import ReWOO, ReWOOPlan, ReWOOStep, ReWOOStepResult

ReWOO (Reasoning WithOut Observation) plans every tool call up front in one model call, runs the tools (in parallel where it can), then writes the answer in a second model call. Use it for tool-heavy tasks where you can predict the calls in advance: it needs far fewer model calls than ReAct. Each plan step is a real tool call, and a step can use an earlier step’s output through a {{En}} placeholder. It is the tool-calling sibling of PlanAndExecute, based on Xu et al. 2023, ReWOO: Decoupling Reasoning from Observations for Efficient Augmented Language Models.

For the conceptual page see ReWOO.


Class signature

class ReWOO: name: str = "rewoo" def __init__( self, *, max_steps: int = 8, planner_prompt: str | None = None, solver_prompt: str | None = None, parallel_levels: bool = True, planner_model: str | Model | None = None, solver_model: str | Model | None = None, ) -> None: ...

Constructor parameters

max_steps

Typeint
Default8

Hard cap on plan length. Truncates to max_steps if the planner emits more. Must be >= 1.

planner_prompt

Typestr | None
DefaultNone (uses built-in default)

Override the planner’s system prompt. The default teaches the model to emit a JSON list of steps, each with id, tool and args, where an argument references an earlier step’s output with a {{En}} placeholder ({En} and the paper’s #En are read too). A {tool_descriptions} placeholder in your prompt is replaced with the tool list. The prompt is filled with str.format when it formats cleanly, which turns a {{En}} you wrote into {En}, so the model learns either form; when it doesn’t (a single-brace JSON example), only {tool_descriptions} is replaced and the rest is left as written.

solver_prompt

Typestr | None
DefaultNone (uses built-in default)

Override the final solver/synthesizer prompt. The default combines the original prompt + the tool results into the final answer. The solver’s message lists each step’s tool and arguments with its output (or its error), each output cut at tool_result_max_chars (50,000 characters).

parallel_levels

Typebool
DefaultTrue

When True (default), independent steps in the same dependency level run concurrently via anyio.create_task_group. Set to False for deterministic test ordering or when your tool provider has tight rate limits and parallel calls trigger throttling.

planner_model / solver_model

Typestr | Model | None
DefaultNone (use the agent’s model)

Give the planner or the solver a different model from the agent’s own. Accepts a model string or a Model instance; role models are not wrapped with the agent’s retry policy. A model string is resolved with the agent’s secrets= backend, like the main model.


Methods

declared_workers

Returns {}. Single-agent.

run

Three-phase loop:

  1. Planner. One LLM call. The output is a JSON list parsed into a ReWOOPlan; a {"steps": [...]} object works too, and the JSON is found inside prose or a code fence. Each step has id, tool and args; args may reference prior steps via {{En}} placeholders or the paper’s #En, and those references define the dependencies. Steps without a tool are dropped, a step without an id gets E<position>, and a repeated id is renamed (references keep meaning the first step with it). Emits rewoo.planner_started / rewoo.plan_created, then rewoo.empty_plan if there are no steps.
  2. Executor. Groups steps into dependency levels; each level runs in parallel when parallel_levels=True (events rewoo.level_started / rewoo.step_completed). Each step’s tool call goes through the same permission, hook and approval gates as a ReAct tool call and emits tool_call / tool_result events. A step whose input step failed, or that references a step the plan doesn’t have, is skipped: it gets an error saying why and no tool call is made. A cyclic plan stops with a rewoo.cyclic_plan event and session.output = "Planner produced a plan with cyclic dependencies; cannot execute.".
  3. Solver. One LLM call combining the original prompt and the tool results into the final answer (rewoo.solver_started, then rewoo.completed).

The budget is checked before the planner, before each level and before the solver.


ReWOOStep

class ReWOOStep(BaseModel): id: str # "E1", "E2", ... tool: str # tool name to call args: dict[str, Any] = {} # args; may contain {{En}} / #En placeholders @property def depends_on(self) -> list[str]: ... # step ids referenced in args

One step in the plan. The framework does textual substitution of {{En}} placeholders with the prior step’s output before running the tool. For complex data flows (extract a URL out of a JSON blob), have the model emit a more structured plan upfront, or fall back to ReAct.

ReWOOPlan

class ReWOOPlan(BaseModel): steps: list[ReWOOStep]

The planner’s output. Stored on session.metadata["rewoo_plan"], with the step results on session.metadata["rewoo_results"].

ReWOOStepResult

class ReWOOStepResult(BaseModel): step_id: str tool: str output: str error: str | None = None

The result of one step. error is non-None (and output empty) when the tool call failed or was refused, or when the step was skipped because an input step failed or it references a step the plan lacks. Steps that use a failed step’s output are skipped rather than run on the error text. (Before 0.17 the error text was substituted into them, and a reference to a missing step was passed through as a literal {{E9}}.)


Cost model

Total: 2 LLM calls + N tool calls. ReAct on the same task needs roughly N+1 LLM calls (one per turn). For tool-heavy workloads where the planner can predict the call sequence upfront, ReWOO is 30–50% cheaper.


When to fall back to ReAct

  • The plan depends on tool output you can’t predict. If a search result reshapes the next call, ReAct’s “think between each tool call” loop is needed.
  • Failure recovery matters. ReWOO commits to the plan; a failed tool call doesn’t trigger replanning, and the steps that need its output are skipped.
  • Tools share state in surprising ways. Parallel-by-default execution can break tools that aren’t idempotent across concurrent calls. Set parallel_levels=False or use ReAct.

Example

from loomflow import Agent, tool from loomflow.architecture import ReWOO @tool async def search(query: str) -> str: """Web search.""" ... @tool async def fetch(url: str) -> str: """Fetch a URL.""" ... agent = Agent( "Research the topic and produce a summary with citations.", model="claude-opus-4-7", architecture=ReWOO(), tools=[search, fetch], ) result = await agent.run("Recent advances in agent harnesses.")

Or by string:

agent = Agent("...", model="claude-opus-4-7", architecture="rewoo", tools=[search, fetch])

Source

loomflow/architecture/rewoo.py

Placeholder substitution is textual. {{E1}} is replaced with E1’s tool result string before the dependent tool runs. Use a structured plan (or ReAct) for complex data flows.

Last updated on