ReWOO
from loomflow.architecture import ReWOO, ReWOOPlan, ReWOOStep, ReWOOStepResultReWOO (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
| Type | int |
| Default | 8 |
Hard cap on plan length. Truncates to max_steps if the planner
emits more. Must be >= 1.
planner_prompt
| Type | str | None |
| Default | None (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
| Type | str | None |
| Default | None (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
| Type | bool |
| Default | True |
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
| Type | str | Model | None |
| Default | None (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:
- 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 hasid,toolandargs; args may reference prior steps via{{En}}placeholders or the paper’s#En, and those references define the dependencies. Steps without atoolare dropped, a step without anidgetsE<position>, and a repeated id is renamed (references keep meaning the first step with it). Emitsrewoo.planner_started/rewoo.plan_created, thenrewoo.empty_planif there are no steps. - Executor. Groups steps into dependency levels; each level
runs in parallel when
parallel_levels=True(eventsrewoo.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 emitstool_call/tool_resultevents. 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 arewoo.cyclic_planevent andsession.output = "Planner produced a plan with cyclic dependencies; cannot execute.". - Solver. One LLM call combining the original prompt and the
tool results into the final answer (
rewoo.solver_started, thenrewoo.completed).
The budget is checked before the planner, before each level and before the solver.
Related types
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 argsOne 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 = NoneThe 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=Falseor 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.