ReWOO's whole trick is that the planner writes the entire plan up front, before a single tool has run, by referring to results it has not seen yet through variables like #E1. The worker then has to figure out what can run now, what must wait, and what can never run at all because the planner hallucinated a reference to a step it never wrote.
A plan step looks like:
{"id": "E2", "tool": "LLM", "args": ["What is the population of #E1?"]}
Every occurrence of #E<n> inside any argument string is a dependency on step E<n>. A step is ready when all of its dependencies have completed in an earlier wave, so:
Two things can make a step unrunnable, and the worker must report them rather than crash:
Either way that step is blocked: it never appears in a wave.
Implement:
def resolve_plan(plan):
Return:
Returns the three-key dict. Note ids sort numerically, not lexicographically - a plain sorted() would put E10 before E2.
plan = [
{"id": "E1", "tool": "Search", "args": ["capital of France"]},
{"id": "E2", "tool": "Search", "args": ["landmarks in Paris"]},
{"id": "E3", "tool": "LLM", "args": ["Summarise #E1 and #E2"]},
]
print(resolve_plan(plan)["waves"])
# [['E1', 'E2'], ['E3']]
E1 and E2 reference nothing, so ReWOO fires them in parallel; E3 waits for both.
plan = [{'id':'E1','tool':'Search','args':['capital of France']},{'id':'E2','tool':'Search','args':['landmarks in Paris']},{'id':'E3','tool':'LLM','args':['Summarise #E1 and #E2']}]
print(resolve_plan(plan)['waves'])[['E1', 'E2'], ['E3']]
E1 and E2 contain no #E references, so both are ready immediately and share wave 0 - this is the parallelism ReWOO buys by planning up front. E3 references both, so it can only run once wave 0 has completed, landing in wave 1.
0 <= len(plan) <= 200; ids are unique and match E<digits>#E<digits> anywhere inside any argument, stringified with str()blocked sort by numeric id index; unresolved sorts lexicographically