Agentic AI · L-Decomp
A chain of dependent steps decays exponentially, and no prompt fixes an exponent. What fixes it is a boundary with a check. Then the plan itself — a DAG the harness owns, scheduled, parallelised and repaired — and the three published architectures that implement it: Plan-and-Execute, ReWOO, LLMCompiler.
Prerequisite Directed graphs, DAGs, topological sort, longest/critical path.
By the end you can:
The case for breaking a task apart is made before any planning machinery, and it is an argument about probability: a chain of dependent steps decays exponentially. Decomposition supplies a boundary between steps — and a boundary only pays once a check sits on it.
Suppose the task needs $n$ dependent steps. Let $X_i$ be the indicator random variable of step $i$, with values $x_i \in \{0,1\}$: $X_i{=}1$ means "step $i$ came out correct". Write $X_{1:i}{=}1$ for "steps $1$ through $i$ are all correct". The task succeeds exactly when $X_{1:n}{=}1$, and the chain rule factorises that conjunction exactly:
$$ P(X_{1:n}{=}1) \;=\; \prod_{i=1}^{n} P(X_i{=}1 \mid X_{1:i-1}{=}1). $$
Assumption. Solved in its own focused context, each step succeeds with the same probability given a correct prefix: $P(X_i{=}1 \mid X_{1:i-1}{=}1) = p$. Then
$$ P(X_{1:n}{=}1) = p^{\,n}. $$
With $p = 0.9$ and $n = 10$ that is $0.9^{10} \approx 0.35$ — worse than a coin flip.
Plain chain-of-thought does not escape this. CoT (Wei et al., 2022) writes the $n$ steps out, but it writes them open-loop, inside one generation: nothing between step $i$ and step $i+1$ inspects the result, so the same $n$ draws still multiply. The comparison this week turns on is therefore not CoT versus no CoT but a chain with no boundary versus a chain with boundaries you can check at.
The exponent is the problem, and no prompt fixes an exponent.
(That the decay is real and not merely arithmetic is measured: Dziri et al. (2023) model a task as a computation graph and grow it — accuracy on multi-digit multiplication, a logic puzzle and a dynamic-programming problem falls to near zero as the graph deepens, even for models fine-tuned on the fully written-out steps. Read $n$ as that graph's depth.)
Three arguments, and only the first one needs a verifier.
Arguments 2 and 3 follow from the structure alone. Argument 1 is an empty promise until something is actually checked at the boundary — which is the rest of Part I.
Why should $p$ be higher once the task is split? A monolithic prompt — all $n$ steps inside a single generation — offers no per-step draw to observe: one sample, one end-to-end verdict. So define the rate it implies as the geometric mean of the same conditionals,
$$ p_{\text{eff}} \;:=\; \Big( \prod_{i=1}^{n} P(X_i{=}1 \mid X_{1:i-1}{=}1) \Big)^{1/n}, \qquad\text{so that}\qquad P(X_{1:n}{=}1) = p_{\text{eff}}^{\,n} $$
holds by construction. The claim decomposition rests on is empirical: $p > p_{\text{eff}}$ — a sub-task solved in its own focused context beats the same stage buried in one long prompt. And because $x \mapsto x^{\,n}$ is steep, a small drop is amplified $n$-fold: at $n = 10$, $p_{\text{eff}} = 0.8$ instead of $0.9$ takes the task from $\approx 0.35$ to $\approx 0.11$.
What the monolithic prompt does wrong (Gulli, 2025): instruction neglect (parts silently ignored) · contextual drift · error propagation · context-window pressure · hallucination under load.
Example. "Analyse this market-research report, summarise it, extract trend data points, and draft an email" — the model may summarise well yet botch the extraction or the email. As a chain: summarise → identify the top-3 trends with their supporting data (from output 1) → draft the email (from output 2). Each step is one focused prompt, with a checkpoint between calls.
Put a check after each sub-task — a test, a schema validation, a critic — and allow one retry. Write $d$ for the probability that the check detects a faulty step. A step then ends correct with
$$ p' \;=\; \underbrace{p}_{\text{right first time}} \;+\; \underbrace{(1-p)\,d\,p}_{\text{caught, and the retry comes out right}} . $$
| $p'$ at $p = 0.9$ | ten steps, $p'^{\,10}$ | |
|---|---|---|
| no check ($d = 0$) | $0.900$ | $0.35$ |
| imperfect check ($d = 0.5$) | $0.945$ | $0.57$ |
| perfect check ($d = 1$) | $0.990$ | $0.90$ |
With a perfect check and $k$ attempts the step fails only if every attempt fails, so $p' = 1 - (1-p)^{k}$. Undetected errors — the fraction $1-d$ — escape immediately, which is a ceiling no number of retries can lift.
A loop can be worth nothing — or less than nothing. The formula above assumed the check only ever fires on a faulty step. Let $\phi$ be the false-positive rate: the probability that the check flags a step that was actually correct. One retry then gives
$$p' \;=\; \underbrace{p\big[(1-\phi) + \phi\,p\big]}_{\text{was correct; wrongly flagged, then redone}} \;+\; \underbrace{(1-p)\,d\,p}_{\text{was faulty; caught, then redone}} \;=\; p \;+\; p\,(1-p)\,(d - \phi).$$
The design rule. A check is worth having exactly when $d > \phi$ — when it catches more real faults than it manufactures. Setting $\phi = 0$ recovers $p' = p + (1-p)dp$.
An ungrounded critic — one that reads only the model's own draft — has $d \approx 0$ while $\phi$ stays positive, so it keeps "fixing" correct work. Strip the unit tests out of Reflexion and the agent starts "performing harmful edits" because it cannot tell it is already done, scoring below the no-loop baseline (Shinn et al., 2023).
Every step produces two things, and only one of them is checkable:
The rule. A verifier earns a detection rate $d$ only when it tests the artifact. Grading the narration measures whether the model sounds correct, which is a different quantity.
Why, measured (Turpin et al., 2023). Plant a misleading hint in the prompt:
The trace is not an audit trail: what moved the answer may never appear in it. Use the chain for task performance and debugging; check the artifact.
| Domain | ❌ Weak check — grading the narration | ✅ Robust check — testing the artifact |
|---|---|---|
| Code generation | "Does this Python logic look correct?" | execute it against unit tests |
| Structured data | reading the explanation of the fields | validate against a JSON Schema |
| Database queries | reading the explanation of the join | run the query in a sandbox; check errors and nulls |
| Maths & calculation | reviewing the intermediate prose | recompute in a Python runtime or SymPy |
| Retrieval / citation | "is this claim supported?" | check each quoted span occurs in the source |
Every robust check has the same shape: it is deterministic, it reads something the model did not write, and its verdict is a value the harness can branch on.
A boundary is a place for a check; reflection is the local loop you put there. It runs on a single step, before that step's output is passed downstream:
The step's own reliability $p$ comes from its executor; the loop's $d$ comes from what the critic is allowed to read.
Split the loop into two roles rather than one self-critiquing model (also generator–critic, producer–reviewer; Gulli, 2025):
| Role | Its job | How it is realised |
|---|---|---|
| Producer | perform the task — write the code, draft the text, produce the plan | the ordinary generating call |
| Critic | evaluate that output against explicit criteria and return structured feedback | a separate call, its own system prompt, usually a distinct persona |
The separation avoids the self-review blind spot: an agent grading its own work brings to the review the same assumptions that produced the error.
And the split that pays is across models, not personas. Chen et al. (2025) optimise which model runs which module of a generator · critic · refiner system. On LiveCodeBench, Claude 3.5 Sonnet is the best single model for the whole system (89 %) — yet as its own critic it "fails to realize its own generation is incorrect", while GPT-4o as the critic "correctly identifies the initial generation is incorrect." Keeping Claude as generator and refiner and swapping only the critic reaches 95 % (CommonGenHard: 75 % → 84 %).
Three primary sources build the same loop and differ in one place — where the critique's evidence comes from. Read together, they measure what that choice is worth:
| Construction | The critique comes from | Measured when it is not grounded |
|---|---|---|
| Self-Refine (Madaan et al., 2023) | the same model, prompted to criticise itself | on maths the feedback is "everything looks good" for 94 % of instances, and the gains all but vanish |
| Reflexion (Shinn et al., 2023) | an Evaluator — exact match, a heuristic, self-generated unit tests | remove the unit tests and HumanEval-Rust falls 60 % → 52 %, below the no-loop baseline |
| CRITIC (Gou et al., 2024) | a tool — search engine, Python interpreter, toxicity API | the model's own critiques are worth −0.03 / +2.33 F1 on QA and −1.8 points on program synthesis |
The rule this yields. "Exclusive reliance on self-correction without external feedback may yield modest improvements or even deteriorate performance" (Gou et al., 2024). The named mechanism is the generation–discrimination–critique gap: a model that can produce a correct answer cannot reliably tell whether the answer it produced is correct. So the design question is never "should I add a reflection loop?" but "what does the critic get to read?"
A chain is only as reliable as the data passed between steps. If one step's output is free-form, the next step can misread it — and that failure is invisible to both.
Fix: each step emits a schema-validated object, and the boundary validates it.
{
"trends": [
{"trend_name": "AI-Powered Personalization",
"supporting_data": "73% of consumers prefer brands that personalise their experience."},
{"trend_name": "Sustainable & Ethical Brands",
"supporting_data": "ESG-claim products grew 28% over 5 years vs. 20% without."}
]
}
(A concrete chain: summarise → extract trends as JSON → draft an email. Three focused prompts, two validated boundaries, instead of one prompt that must do all three — Gulli, 2025.)
Given that we want boundaries, the rest is engineering. This part asks where the plan lives. In a ReAct loop it exists only as the accumulated transcript; the moment it becomes an object outside any single generation, the harness can inspect it, validate it, schedule it, show it to a user, and retry one step of it alone.
Three published architectures build that object three different ways — Plan-and-Execute, ReWOO and LLMCompiler — and the differences between them are exactly the design decisions you will have to make.
Agent planning runs at two separate scopes, and most confusion about "which pattern should I use?" comes from mixing them.
The two levels are filled from different menus, and the asymmetry is the point: what sits outside is a way of splitting a goal; what sits inside is a way of acting.
| Outer — how is the goal split? | Inner — how is one sub-task finished? |
|---|---|
| Just-in-time — decide the next step each turn (ReAct); no plan object | a deterministic call — a tool, a code execution, a schema-validated parse |
| Plan-and-solve — the whole plan up front, then execute it | a single CoT generation |
| Recursive — decompose a sub-task that is still too big | a ReAct loop — reason, act, observe |
| Search over decompositions — score candidates, explore the good ones (ToT, → L-ToT) |
Choosing an executor: take the most deterministic one the sub-task admits.
The exponent in $p^{\,n}$ lives at the inner level, so the cheapest way to raise a chain's reliability is not a better prompt but a step that cannot be got wrong — a tool call where the answer is computable, a schema-validated parse where the question is one of format, and a model generation only where judgment is genuinely required.
One measurement. Wei et al. (2022) categorise 50 failed chains on a maths benchmark by what minimal change would fix them: 8 % were correct except for a calculator error. That is not a reasoning failure at all — it is a step handed to the wrong executor, and swapping in an external calculator lifted that model's accuracy from 14.3 to 17.3. Part of what looks like a low per-step $p$ is a routing decision.
And the executor is a subroutine call, not a delegation. The harness calls the inner program; the call has a frame. The sub-task's intermediate observations are that frame's locals — they live for the duration of the call and are discarded on return, so what crosses the boundary is the sub-task's artifact, never its transcript. That is what keeps the outer level's context proportional to the number of steps rather than to the work done inside them.
What the composition licenses, and the one arrangement it does not.
The measured nesting is plan or search outside, act inside:
| outer ↓ · inner → | deterministic call | CoT | ReAct loop |
|---|---|---|---|
| Plan-and-solve (a plan object exists) | ✓ | ✓ | ✓ — ADaPT (Prasad et al., 2024), measured |
| Search (ToT / MCTS) | ✓ | ✓ | ✓ — LATS (Zhou, Andy, et al., 2024), measured |
| Just-in-time ReAct (no plan object) | ✓ — this is ReAct | ✓ | ✗ |
The single ✗ is not a prohibition but a consequence: a ReAct loop nested in a ReAct loop that shares the parent's transcript has no frame and therefore no return — the inner turns are outer turns, nothing is discarded, and the goal is diluted along a trajectory that lengthens with every level (Huang et al., 2024). Give the inner loop its own context, prompt and tools and the frame exists — but then it is a subagent, a different design with a different bill (→ L-MAS).
ADaPT (Prasad et al., 2024) is the clean instance of the composition: a controller — "a pre-determined and recursive algorithm", ordinary code — runs a ReAct executor on the whole task first and calls a planner to split it into 3–5 sub-steps only when that executor fails, recursing to depth $d_{\max}$. Decomposition is a failure handler, not an opening move, and "with $d_{\max} = 1$, ADaPT solely relies on this executor" — at depth 1 the system is plain ReAct. Measured on GPT-3.5: ALFWorld 43.3 → 71.6, TextCraft 19.0 → 52.0, WebShop 17.0 → 44.0.
Every pseudocode box this week is written against the same names, so that the differences between them are the only thing that changes. The shared names are collected here; the three boxes that add names of their own — ReWOO, LLMCompiler and the execution scheduler — list them immediately before their own box.
| Symbol | Reads |
|---|---|
| $\textit{goal}$ | what the user asked for — the input to the whole run. Every box uses this name — ReWOO's paper writes task for it and LLMCompiler's user input; the boxes are re-lettered, so that in the pseudocode a task is always a node of the plan |
| $\tau_1, \dots, \tau_n$ | the sub-tasks of the plan; $n$ is how many the planner emitted. $\tau$ is one sub-task, $\tau'$ a retry of it (a new node) |
| $r_i$, $r_\tau$ | the result (the artifact) of a sub-task — never its transcript. A step that fails yields no $r$: what it returns is a failure report, and the scheduler box keeps the two apart |
| $\textit{results}$ | the list of $(\tau_i, r_i)$ pairs finished so far — successes only. The scheduler box needs a second store for what was merely attempted, and writes it $\textit{out}$ |
| $\text{LLM}_{\text{plan}}(\cdot)$, $\text{LLM}_{\text{replan}}(\cdot)$, $\text{LLM}_{\text{answer}}(\cdot)$, $\text{LLM}_{\text{solve}}(\cdot)$, $\text{LLM}_{\text{join}}(\cdot)$ | one model call, subscripted by the role it plays — the same model may fill several |
| $\text{Solve}_i(\cdot)$, $\text{Solve}_\tau(\cdot)$ | the executor of one sub-task: a tool call, a CoT generation, or a whole ReAct loop. The subscript is not decoration — which executor runs is a decision per sub-task (see Choosing an executor), and it is what sets that step's $p$. Plan-and-Execute writes $\text{ReAct-Executor}(\cdot)$ instead, because it fixes one such executor for every step |
| $\text{Render}(\cdot)$ | builds a step's prompt from the goal, the plan and the results so far — the $\textit{prompt}_i$ that is handed to $\text{Solve}_i$ |
| $a$ | what a replanning call returns: either a Response (finish now) or a new Plan; $a.\textit{steps}$ are its steps and $a.\textit{context}$ what the attempt established |
| $\varnothing$ | nothing yet — an unset value. (The empty set is written $\emptyset$.) |
⚠ And $\Theta$ carries two. Here it is asymptotic cost — $\Theta(V+E)$ for Kahn's algorithm. In ReWOO's token equations it is that paper's notation for a token count. The two are unrelated; each use says which is meant.
Everything in this part is a variation on one control structure, so it is worth having that structure in front of you before the variations. The agent loop of → L-ReAct and this one differ in a single line (1).
$$ \begin{array}{l} \textbf{Decompose-then-Solve}(\textit{goal}) \\ \hline \textbf{Input: } \textit{goal} \text{; the tool set} \\ \textbf{Output: } \text{the final answer} \\ \hline 1: \quad [\,\tau_1, \dots, \tau_n\,] \gets \text{LLM}_{\text{plan}}(\textit{goal}) \qquad \text{// ONE call, before any action is taken} \\ 2: \quad \textit{results} \gets [\;] \qquad \text{// one entry per finished step — results, not transcripts} \\ 3: \quad \textbf{for } i = 1 \dots n \textbf{ do} \qquad \text{// the loop is bounded by the PLAN, not by a turn budget} \\ 4: \quad \qquad \textit{prompt}_i \gets \text{Render}\big(\textit{goal},\ [\tau_1 \dots \tau_n],\ \tau_i,\ \textit{results}\big) \\ 5: \quad \qquad r_i \gets \text{Solve}_i(\textit{prompt}_i) \qquad \text{// the executor CHOSEN FOR THIS SUB-TASK — a tool call, a CoT generation, a ReAct loop} \\ 6: \quad \qquad \textit{results}.\text{append}\big((\tau_i,\, r_i)\big) \\ 7: \quad \textbf{return } \text{LLM}_{\text{answer}}(\textit{goal},\ \textit{results}) \qquad \text{// one synthesis call} \\ \hline \end{array} $$
Line 1 is the whole difference. The ReAct loop has no counterpart to it: there the plan exists only as the accumulated history. Here it is an object, produced before anything is executed — which is what makes it inspectable, schedulable, approvable and retryable step by step.
Three consequences fall out of that one line:
And five places carry every variation that follows. The three published architectures change some of these, and nothing else. Each gets a name here, because every architecture below numbers its own box differently — "line 4" means something else in each of them:
| Place | Where in the box | The question it answers |
|---|---|---|
| Plan time | line 1 | when is the plan made — once, streamed, or again every round? |
| Loop domain | line 3 | what does the loop walk — all $n$ steps in order, only the first, or the ready set (those whose inputs are all available)? |
| Input binding | line 4 | how does a step receive its inputs — re-rendered prose, or a substituted variable? |
| Executor choice | line 5 | who picks the executor — the harness, per sub-task; or the planner, by naming a tool? |
| Replanning | a back-edge to line 1 | is there one at all, and how coarse is it? |
In this section, we analyze three primary agent planning strategies:
Explicit Plan Object:
Think -> Act -> Observe loop, deciding one step at a time.Plan Structure:
#E1, #E2) to pass intermediate outputs between steps.Replanning Strategy:
Scheduler for LLMCompiler only:
Nodes: planner → agent → replan → {agent, END}. The name says plan and execute; the graph is
plan → execute one → replan the remainder (LangGraph, 2025).
$$ \begin{array}{l} \textbf{Plan-and-Execute}(\textit{goal}) \\ \hline \textbf{Input: } \textit{goal} \text{; the tool set} \\ \textbf{Output: } \text{the final answer} \\ \hline 1: \quad [\,\tau_1, \dots, \tau_n\,] \gets \text{LLM}_{\text{plan}}(\textit{goal}) \\ 2: \quad \textit{results} \gets [\;] \\ 3: \quad \textbf{loop} \qquad \text{// NOT } \textbf{for } i = 1 \dots n \text{ — the plan is replaced, never walked} \\ 4: \quad \qquad \tau \gets \tau_1 \qquad \text{// only the FIRST step of the current plan is executed} \\ 5: \quad \qquad r \gets \text{ReAct-Executor}\big(\tau,\ [\tau_1 \dots \tau_n]\big) \qquad \text{// a subroutine call with its own frame} \\ 6: \quad \qquad \textit{results}.\text{append}\big((\tau,\, r)\big) \\ 7: \quad \qquad a \gets \text{LLM}_{\text{replan}}\big(\textit{goal},\ [\tau_1 \dots \tau_n],\ \textit{results}\big) \qquad \text{// a Response, or a new Plan} \\ 8: \quad \qquad \textbf{if } a \text{ is a Response } \textbf{then return } a \\ 9: \quad \qquad [\,\tau_1, \dots, \tau_n\,] \gets a.\textit{steps} \qquad \text{// "only what still NEEDS to be done"} \\ \hline \end{array} $$
ReWOO (Xu et al., 2023) removes the replanning entirely: the plan is written before any tool runs, and never revised.
| Module | What it does | What it sees |
|---|---|---|
| Planner | "composes a solution blueprint" — consecutive tuples $(Plan, \#E_s)$, where $\#E_s$ is "a special token to store presumably correct evidence" of step $s$ | the task and the prompt — no observations |
| Worker | executes one step's tool call and populates its $\#E_s$ | one step's instruction |
| Solver | "processes all plans and evidence to formulate a solution" | the task, plus plans + evidence, paired |
The capability this demands of the planner has a name: foreseeable reasoning — reasoning about what a step will return without seeing it.
ReWOO adds the following to the shared list under Notation for the Pseudocode; none of them appears in any other box.
| Symbol | Reads |
|---|---|
| $P$ | the plan as text — what the planner emits, prior to parsing |
| $\text{Parse}(\cdot)$ | splits that text into the $n$ steps, one tuple $(\tau_s, e_s, \textit{tool}_s, \textit{args}_s)$ each |
| $e_s$ | the name of step $s$'s evidence slot — the paper writes it #E$s$ |
| $\textit{tool}_s$, $\textit{args}_s$ | what step $s$ calls, and the argument string it calls with — still carrying the #E names of the steps it depends on |
| $\text{Substitute}(\cdot)$ | replaces each such name in that string by the value stored under it, yielding the $\textit{args}$ actually passed. This substitution is the edge |
| $\textit{evidence}$ | the evidence store: name $\mapsto$ the result filled into it |
ReWOO, written out. The three modules are lines 1, 4–6 and 7.
$$ \begin{array}{l} \textbf{ReWOO}(\textit{goal}) \\ \hline \textbf{Input: } \textit{goal} \text{; the tool set} \\ \textbf{Output: } \text{the final answer} \\ \hline 1: \quad P \gets \text{LLM}_{\text{plan}}(\textit{goal}) \qquad \text{// ONE call — the PLANNER; plan text, never re-entered} \\ 2: \quad \big[\,(\tau_s, e_s, \textit{tool}_s, \textit{args}_s)\,\big]_{s=1}^{n} \gets \text{Parse}(P) \qquad \text{// step text, evidence NAME, tool, argument string} \\ 3: \quad \textit{evidence} \gets \{\;\} \qquad \text{// the evidence store: name} \mapsto \text{result} \\ 4: \quad \textbf{for } s = 1 \dots n \textbf{ do} \qquad \text{// strictly the planner's order — nothing is scheduled} \\ 5: \quad \qquad \textit{args} \gets \text{Substitute}(\textit{args}_s,\ \textit{evidence}) \qquad \text{// a name in the argument string} \mapsto \text{its value — THE EDGE} \\ 6: \quad \qquad \textit{evidence}[e_s] \gets \textit{tool}_s(\textit{args}) \qquad \text{// the WORKER runs one tool} \\ 7: \quad \textbf{return } \text{LLM}_{\text{solve}}\big(\textit{goal},\ [\tau_1 \dots \tau_n],\ \textit{evidence}\big) \qquad \text{// the SOLVER} \\ \hline \end{array} $$
Against the skeleton. Plan time and loop domain are unchanged — one call up front, all $n$
steps in order — and there is no replanning at all. The one real change is the input binding:
instead of re-rendering a prompt from results, a step's arguments are completed by substituting a
named value. That single change is what turns the plan into a graph. (Executor choice moves into
the plan too: the planner names $\textit{tool}_s$ per step.)
The edges are variable bindings. A later step depends on an earlier one "by referring to $\#E_s$ from previous steps in the instructions given to Workers" — and that reference is the edge:
Plan: Find Company A's 2024 revenue.
#E1 = Search["Company A 2024 annual revenue"]
Plan: Find Company B's 2024 revenue.
#E2 = Search["Company B 2024 annual revenue"]
Plan: Compute the ratio of the two.
#E3 = Calculator["#E1 / #E2"]
This is worth dwelling on, because it removes a whole class of bug. A format that passes results
by name derives its dependency graph from the text itself: step 3 depends on steps 1 and 2
because it reads #E1 and #E2. A format that declares dependencies in a separate depends_on list states the same fact twice — and can contradict itself.
The plan above is already a DAG. ReWOO does not exploit it: the Worker runs the steps in the order
the planner listed them, so #E1 and #E2 are executed serially even though nothing connects
them. Removing exactly that limitation is what the next architecture is for.
LLMCompiler (Kim et al., 2024) keeps ReWOO's variable bindings, makes the DAG explicit, and dispatches every task the moment its dependencies resolve. The paper's vocabulary is a compiler's, deliberately.
1. search("Company A 2024 annual revenue")
2. search("Company B 2024 annual revenue")
3. math("$1 / $2")
4. join()
Tasks 1 and 2 have no dependencies, so they run at the same time; task 3 waits for both. The Task Fetching Unit is "inspired by the instruction fetching units in modern computer architectures", and "fetches tasks to the Executor as soon as they are ready for (parallel) execution based on a greedy policy" — which is exactly the ready set of the scheduler in Part III.
LLMCompiler adds the following to the shared list under Notation for the Pseudocode; none of them appears in any other box.
| Symbol | Reads |
|---|---|
| $G = (T, E)$ | the plan as a DAG, arriving as a stream: its nodes $T$ are the sub-tasks the planner emits, its edges $E$ are what their $\textit{deps}$ fields declare — so the dependency graph is not something the scheduler must be told separately, it is already there. The superscript in $\text{LLM}^{\text{stream}}_{\text{plan}}$ records that the caller may begin consuming it before the call has completed, so $G$ grows while it is being scheduled |
| $\tau = (\textit{idx}, \textit{tool}, \textit{args}, \textit{deps})$ | one streamed sub-task — its index, the tool it calls, its argument string with $-placeholders, and the indices it waits for |
| $\textit{obs}$ | the observation map: task index $\mapsto$ that task's output |
| $\operatorname{deps}(\tau)$, $\operatorname{dom}(\textit{obs})$ | the indices $\tau$ waits for, and the indices already filled in — so "is it ready?" is the containment $\operatorname{deps}(\tau) \subseteq \operatorname{dom}(\textit{obs})$ |
| $\text{Schedule}(\cdot)$ | the Task Fetching Unit: it dispatches a task as soon as that containment holds and substitutes the placeholders — a ready set kept without an in-degree counter. It is the scheduler written out under Execution, Failure & Recovery |
| $\textit{context}$ | what a replanning round returns to the planner — "context from the last attempt"; $\varnothing$ on the first round |
$$ \begin{array}{l} \textbf{LLMCompiler}(\textit{goal}) \\ \hline \textbf{Input: } \textit{goal} \text{; the tool set} \\ \textbf{Output: } \text{the final answer} \\ \hline 1: \quad \textit{obs} \gets \{\;\} \qquad \text{// the observation map: task index} \mapsto \text{that task's output} \\ 2: \quad \textit{context} \gets \varnothing \\ 3: \quad \textbf{loop} \\ 4: \quad \qquad G \gets \text{LLM}^{\text{stream}}_{\text{plan}}(\textit{goal},\ \textit{context}) \qquad \text{// STREAMS a DAG: nodes } \tau = (\textit{idx},\ \textit{tool},\ \textit{args},\ \textit{deps}) \text{, EDGES the } \textit{deps} \\ 5: \quad \qquad \textit{obs} \gets \text{Schedule}(G,\ \textit{obs}) \qquad \text{// ready-set dispatch — runs tasks IN PARALLEL} \\ 6: \quad \qquad a \gets \text{LLM}_{\text{join}}(\textit{goal},\ \textit{obs}) \qquad \text{// the JOINER: a Response, or Replan} \\ 7: \quad \qquad \textbf{if } a \text{ is a Response } \textbf{then return } a \\ 8: \quad \qquad \textit{context} \gets a.\textit{context} \qquad \text{// what this attempt learned — RECOMPILATION} \\ \hline \end{array} $$
The Joiner is line 6 of this box, and the parallelism is all inside
Schedule — which Execution, Failure & Recovery opens up.
Against the skeleton. Plan time becomes a stream. Loop domain is replaced by
Schedule, which walks the ready set instead of an index and therefore runs independent tasks at
once. Input binding is a placeholder substitution, performed inside Schedule. Executor
choice sits in the plan, as $\operatorname{tool}(\tau)$. And replanning fires only when the
Joiner asks for it, not once per step.
Two mechanisms beyond the DAG, and both matter in practice.
Measured against a ReAct baseline — and note that both latency and cost improve, because the plan removes LLM invocations rather than adding them:
| Benchmark | Latency | Accuracy |
|---|---|---|
| HotpotQA (2-way parallel) | 1.80× | equal |
| Movie Recommendation (8-way parallel) | 3.74× | 72.47 → 77.13 |
| ParallelQA (dependency-heavy) | 2.15× | on LLaMA-2 70B, 59.59 → 68.14 |
| Game of 24, vs. Tree-of-Thoughts | 2.89× | 74.00 → 75.33 |
Cost falls 3.37× / 6.73× / 4.65× on the three benchmarks, "because it involves less frequent LLM invocations."
The columns are four of the five named places; executor choice follows underneath.
| Plan time | Loop domain | Input binding | Replanning | The cost it pays | |
|---|---|---|---|---|---|
| Decompose-then-Solve (the bare skeleton, after Zhou et al., 2023) | once, up front | all $n$ steps, in order | re-rendered from results |
none | a plan that goes stale cannot be repaired |
| Plan-and-Execute | once, then again every round | only the first step | the whole plan, in the executor's prompt — not the goal, not the results | every round | one planner call per step; "restricted to serial tool calling" |
| ReWOO | once, up front | all $n$ steps, in the planner's order | variable substitution — #E1 inside the argument string |
none | the planner must foresee; an unknown environment forces enumeration; and with no caller loop there is nowhere to send a hard failure — the run simply ends |
| LLMCompiler | streamed, re-entered on a Joiner verdict | the ready set — in parallel | placeholder substitution — $1 from the observation map |
on demand (recompilation) | a streamed graph cannot be topologically sorted |
And executor choice, the fifth. The bare skeleton lets the harness pick one per sub-task.
Plan-and-Execute fixes a single ReAct agent for all of them. ReWOO and LLMCompiler move
the choice into the plan: the planner names the tool for each step, which is why their boxes write
$\textit{tool}_s$ and $\operatorname{tool}(\tau)$ rather than a Solve. It is the deepest of the
five differences — it decides whether "which executor?" is a harness policy or something the model
gets to hallucinate.
A decision rule.
Who authors the plan's structure is a decision separate from how it is executed — and it decides where that structure's reliability comes from.
The design question (Gulli, 2025): does the "how" need to be discovered, or is it already known? Known and repeatable → fix the workflow. Must be discovered → a planning agent. Don't grant autonomy you don't need.
| Provenance | Who fixes the structure | Structural reliability | Adaptivity |
|---|---|---|---|
| Generated — the model writes the plan | the model, at runtime | as good as the model's dependency reasoning | full |
| Fixed — a workflow in code | a programmer, before the run | by construction: a graph in code cannot be hallucinated | only the branches that were written |
| Hybrid — fixed graph, agentic nodes | code fixes the graph; the model fills slots and acts inside a node | structure by construction, content by the model | inside a node |
The rows differ in what the exponent ranges over. If the graph is code, no step can depend on an edge that does not exist, and $p^{\,n}$ is a statement about the content of the steps alone. If the model emits the graph, its structure is one more thing that can be wrong — and it is wrong before the first step runs.
Three structural defects, none of which the model signals:
The size of the effect is measured, and it is not small. On Blocksworld with ≤ 5 blocks — a domain whose actions, preconditions and effects are stated in the prompt — GPT-3 produced a valid plan in 1 % of instances and Instruct-GPT3 in 6.8 %, against 78 % for human participants (Valmeekam et al., 2023).
So a generated plan is itself an artifact to be checked. Validate it against the tool catalogue and the step preconditions before executing anything — the cheapest verification available, because it runs once, before any step costs money.
Production systems sit at neither end. The common arrangement is a hard-coded execution graph in which selected nodes are agentic: the edges, the branch conditions and the retry budget live in code, and a node facing an unpredictable environment runs a short-horizon ReAct loop with its own context, tools and step cap.
The outer level here is a program, not a model; the model appears only as an executor inside a node. What the hybrid buys is that autonomy is scoped — it exists where the path genuinely has to be discovered, and nowhere else. Mostly workflow, selectively agentic.
Plan approval — human in the loop. A deep research agent writes a plan, then works it with retrieval tools for tens of steps. What is new in it is not the machinery — ReAct over a plan, with reflection and retrieval — but the gate: the plan is shown to the user to review and edit before execution, so the long and expensive part starts from an approved plan. A plan object is what makes such a gate possible at all.
All three hand the harness a plan object; two of them hand it an explicit dependency graph. This part is what a runtime does with that graph: order it, run what can run at once, price the parallelism, and repair it when a step comes back wrong.
Nodes are sub-tasks $\tau_1,\dots,\tau_n$; a directed edge $\tau_i \to \tau_j$ means "$\tau_i$ must finish before $\tau_j$" — a dependency, and in practice a data flow. A sensible plan has no cycles, so it is a Directed Acyclic Graph.
What a real planner stores is the adjacency list — one record per sub-task with its predecessors (
blockedBy): $\Theta(V+E)$ space and $\Theta(1)$ work per completed prerequisite. Plan DAGs are sparse, so the matrix is for the maths, not for the implementation (Cormen et al., 2022, §20.1).
🔁 Prior knowledge (your Algorithms course): Grundlagen der Graphen · Graphensuche (BFS/DFS) · Dijkstra.
Static plan vs. dynamic execution graph — a common confusion, and it is worth keeping straight before anything is scheduled.
- The static plan you schedule is a DAG: $\tau_1 \to \tau_2 \to \tau_3$.
- The execution trace contains cycles: $\tau_2 \to \texttt{tests\_failed} \to \tau_2' \to \tau_3$ (try → fail → retry).
They are reconciled by unrolling: each retry becomes a new node ($\tau_2'$ = "attempt 2"), so the expanded graph stays acyclic and schedulable. The cycle lives in the agent's control loop, not in the DAG you topologically sort.
| Operation | What it does | Typical trigger |
|---|---|---|
| Insert | add nodes — a retry attempt, or a coarse node expanded into a sub-DAG | a step failed, or turned out to be too big |
| Prune | drop nodes no longer reachable or no longer needed | a branch was abandoned; a hard failure blocked a cone |
| Re-edge | add or remove a dependency | an observation revealed a data dependency the planner did not know about |
Two invariants keep this safe to do to a graph a scheduler is already walking:
A topological sort orders the sub-tasks so that every edge $\tau_i \to \tau_j$ points forward. That is exactly a legal order to execute the plan.
Existence. A topological order exists iff the graph is a DAG. A cycle has no valid starting point, so no ordering can respect all its edges.
Kahn's algorithm, in one sentence: start with all sources, emit one, remove it, decrement its successors' in-degrees, and any successor that reaches in-degree 0 becomes newly available. Emit all $n$ nodes ⇒ it is a DAG; get stuck with nodes left ⇒ there is a cycle.
Cost. $\Theta(V+E)$ time, $\Theta(V)$ extra space on an adjacency list — one pass for the in-degrees, then every node dequeued once and every edge relaxed once (Kahn, 1962; Cormen et al., 2022, §20.4).
The ready set is what may be executed immediately — every sub-task not yet executed whose inputs are all available. In the residual graph (the plan minus the finished nodes) those are the in-degree-zero nodes, which is why Kahn's algorithm is an execution scheduler's loop. That set, not the sorted list, is what a scheduler actually consumes.
Producing vs. checking. Verifying a given order is one pass over the edges — check $\mathrm{pos}(\tau_i) < \mathrm{pos}(\tau_j)$ for every edge — simpler than the algorithm that produced it, and independent of it. Cheap checks on expensive steps: the same move as a verifier on a decomposition boundary.
$$ \begin{array}{l} \textbf{Kahn-TopoSort}(G) \\ \hline \textbf{Input: } \text{directed graph } G = (V, E) \\ \textbf{Output: } \text{a topological ordering of } V \text{, or } \text{CycleDetectedError} \\ \hline 1: \quad \textbf{for each } v \in V \textbf{ do } inDegree[v] \gets 0 \\ 2: \quad \textbf{for each } (u, v) \in E \textbf{ do } inDegree[v] \gets inDegree[v] + 1 \\ 3: \quad Q \gets \text{empty queue}, \quad \textit{order} \gets \text{empty list} \\ 4: \quad \textbf{for each } v \in V \textbf{ do} \\ 5: \quad \qquad \textbf{if } inDegree[v] = 0 \textbf{ then } Q.\text{enqueue}(v) \\ 6: \quad \textbf{while } Q \text{ is not empty} \textbf{ do} \\ 7: \quad \qquad u \gets Q.\text{dequeue}() \qquad \text{// } Q \text{ IS the ready set} \\ 8: \quad \qquad \textit{order}.\text{append}(u) \\ 9: \quad \qquad \textbf{for each } (u, v) \in E \textbf{ do} \\ 10: \quad \qquad\quad inDegree[v] \gets inDegree[v] - 1 \\ 11: \quad \qquad\quad \textbf{if } inDegree[v] = 0 \textbf{ then } Q.\text{enqueue}(v) \\ 12: \quad \textbf{if } |\textit{order}| = |V| \textbf{ then return } \textit{order} \\ 13: \quad \textbf{else raise } \text{CycleDetectedError} \\ \hline \end{array} $$
Why Kahn and not the DFS reverse-postorder sort (both $\Theta(V+E)$): Kahn yields the frontier incrementally, so the ready set is available during the run; DFS only produces an order after the whole traversal, and grouping independent steps out of it takes a second pass. Kahn's queue also swaps for a priority queue without changing anything else, and it is iterative — no $O(V)$ call stack on a deep plan. DFS keeps one advantage: on a cycle its recursion stack holds the offending path, which Kahn cannot report (Kahn, 1962; Tarjan, 1976).
A topological order gives a sequential schedule — but the DAG also says what can run at the same time. Independent sub-tasks (no path between them) execute in parallel.
The critical path is the longest-duration path through the DAG. With unlimited workers the minimum time to finish — the makespan — is the cumulative duration along it:
$$ \text{makespan} \;=\; \max_{\pi \in \Pi} \sum_{\tau_i \in \pi} d_i $$
where $\Pi$ is the set of all directed paths, $\pi$ a path $(\tau_{i_1}, \dots, \tau_{i_k})$ following the edges, and $d_i$ the duration of task $\tau_i$.
🔁 Longest path on a general graph is NP-hard; on a DAG it is $\Theta(V+E)$ by topological sort + dynamic programming. (Critical path, slack and makespan are the Critical Path Method — Kelley & Walker, 1959.)
Request: "Fetch the sales dataset, write a function that computes the per-group mean, test it,
and report." The planner — here the LLM itself, prompted to emit JSON — answers with structured
steps, one object per sub-task, each naming what it depends_on. Those fields are the edges,
so the parsed plan is this graph. The second number on each node is its duration.
read_spec. Sink: report.fetch_data and write_tests depend only on read_spec → they can run in parallel.run_tests needs both write_code and write_tests — a fan-in.The planner supplies the graph, not the weights. The model emits
idanddepends_on— what needs what, which it can know from the task. It does not emitdur: an LLM has no calibrated sense of how long a step takes, and the critical path and every "speeding this up saves nothing" verdict are properties of exactly those weights. Durations come from measurement — the latency of prior runs of the same step type (→ L-Eval).
import numpy as np
from collections import deque
plan = [ # the LLM planner's JSON answer, parsed: ids + edges only
{"id": "read_spec", "depends_on": []},
{"id": "fetch_data", "depends_on": ["read_spec"]},
{"id": "write_code", "depends_on": ["fetch_data"]},
{"id": "write_tests", "depends_on": ["read_spec"]},
{"id": "run_tests", "depends_on": ["write_code", "write_tests"]},
{"id": "report", "depends_on": ["run_tests"]},
]
cost_model = { # NOT from the model: measured from prior runs / a cost model
"read_spec": 1, "fetch_data": 3, "write_code": 4,
"write_tests": 2, "run_tests": 1, "report": 1,
}
tasks = [s["id"] for s in plan]
dur = np.array([cost_model[t] for t in tasks]) # weights joined onto the graph by id
deps = [(d, s["id"]) for s in plan for d in s["depends_on"]] # depends_on == the edges
n = len(tasks); ix = {t: i for i, t in enumerate(tasks)}
A = np.zeros((n, n), dtype=int)
for a, b in deps:
A[ix[a], ix[b]] = 1 # A[i,j]=1 <=> task i must precede task j
# --- Kahn's topological sort: the queue IS the ready set ---
indeg = A.sum(axis=0).copy() # column sums = in-degrees
ready = deque(i for i in range(n) if indeg[i] == 0)
order = []
while ready:
i = ready.popleft()
order.append(i)
for j in np.nonzero(A[i])[0]: # successors of i
indeg[j] -= 1
if indeg[j] == 0:
ready.append(j) # newly unblocked -> may run now
assert len(order) == n, "cycle detected - not a DAG"
print("execution order :", [tasks[i] for i in order])
# --- verify the result: every edge must point forward in the order (Theta(V+E)) ---
pos = np.empty(n, int)
for k, i in enumerate(order):
pos[i] = k
assert all(pos[i] < pos[j] for i, j in zip(*np.nonzero(A))), "not a topological order"
print("plan validated : every edge points forward")
# --- critical path: longest path by DURATION, DP over the topological order ---
finish = dur.copy() # finish[i] = earliest time task i can be done
for i in order:
for j in np.nonzero(A[i])[0]: # relax every edge, forwards
finish[j] = max(finish[j], finish[i] + dur[j])
makespan = int(finish.max())
# --- slack: how late may a task finish without delaying the plan? (backwards pass) ---
latest = np.full(n, makespan) # sinks may finish at the makespan
for i in reversed(order):
succ = np.nonzero(A[i])[0]
if len(succ):
latest[i] = min(latest[j] - dur[j] for j in succ)
slack = latest - finish # slack == 0 <=> on the critical path
for i in order:
mark = " <-- critical" if slack[i] == 0 else ""
print(f" {tasks[i]:<12} dur={dur[i]} earliest finish={finish[i]:>2} slack={slack[i]}{mark}")
print("critical path :", " -> ".join(tasks[i] for i in order if slack[i] == 0))
print(f"makespan : {makespan} (serial sum would be {int(dur.sum())})")
Reading the schedule off the graph.
read_spec → fetch_data → write_code → run_tests → report, of length
$1+3+4+1+1 = \mathbf{10}$ — the plan cannot finish sooner, however many workers you have. The
serial sum is 12, so parallelism buys 2.write_tests is off it and has 5 steps of slack: it may start late, or take up to $2+5=7$
steps, before the finish time moves at all.A wrong edge does not crash — it quietly removes freedom. Why does
fetch_dataprecedewrite_codebut notwrite_tests? An edge is a claim about what a step needs as input, not a guess about what will run first. The code must match the dataset's columns and quirks, so it needs the data in hand; the tests encode the contract from the spec, which is known before any data arrives. Model it the other way and the makespan stays 10 — butwrite_testsloses its independence and its slack falls from 5 to 2. That is the spurious edge defect, seen from the scheduler's side.
A topological order says only what is legal to run. When several tasks are ready at once and workers are limited, the scheduler must rank them.
Scheduling vs. prioritization. Topological sort answers "what may run?"; prioritization answers "of the ready ones, which first?" — and the second only matters under resource constraints.
Data flows along the edges: $\tau_j$ receives the outputs of every $\tau_i \to \tau_j$ as its inputs. The scheduler walks the ready set, running independent nodes in parallel and threading outputs into their dependents.
Two different things are called "failure", and the difference decides who can repair them.
| What happened | What the run does with it | |
|---|---|---|
| A verdict | the step ran and returned a negative result — tests red, the checker rejected the draft | nothing is broken; this is the verifier doing its job. The replanner extends the plan with a retry node |
| A hard failure | the step produced no output — the API was down, the file was missing | nothing downstream can run: the scheduler blocks the descendant cone, and only a new plan can recover it |
A retry is a new node, not a revisited one. Feeding the error back changes the input, so the second attempt is a different task instance $\tau'$ — the unrolling rule, now as an execution rule rather than a drawing convention. Cap the unrolling with a retry cap $\kappa$ — a limit on how often a run may re-plan around the same failure — or the plan grows forever. The cap belongs to the caller, because the unrolling does.
The scheduler neither plans nor repairs. $G$ is a parameter of the box below, not something it produces: what follows is the runtime a planning architecture hands its plan to — the contents of the $\text{Schedule}$ call the LLMCompiler box leaves closed. Both the replanning and the unrolling therefore stay with the caller, and the scheduler returns what it could not run instead of answering around it.
The scheduler adds its own names to the shared list under Notation for the Pseudocode; none of them appears in any other box.
| Symbol | Reads |
|---|---|
| $G = (T, E)$ | the plan as a DAG: $T$ its sub-tasks (the nodes), $E$ its dependency edges. It is an input — the scheduler runs a plan, it neither makes nor repairs one |
| $\operatorname{pred}(\tau)$ | the predecessors of $\tau$ — the tasks whose results flow into it along the edges |
| $\operatorname{desc}(\tau)$ | the descendant cone of $\tau$: every task reachable from it, i.e. everything a failure blocks. $\tau$ itself is not in it — a failed task is kept out of the ready set by $\textit{done}$ instead, and that is what makes the loop terminate |
| $\textit{out}$ | what each attempted sub-task produced, keyed by node: its artifact, or — where it failed — its failure report. That report is a distilled account of what went wrong, never the failed transcript, and writing it is $\text{Solve}_\tau$'s job. It has two readers: the replanner, which diagnoses from it, and the join call that composes the answer. A raw transcript here would poison both |
| $\textit{done}$ | the sub-tasks already attempted, whatever they returned; equivalently $\operatorname{dom}(\textit{out})$. Only verified results are handed on as $\textit{results}$, so the two are not the same set |
| $\textit{ready}$ | the ready set: the sub-tasks that may be executed immediately — not yet attempted, not blocked, and with every input available |
| $\text{ReadySet}(\cdot)$ | recomputes it from the run's state: not in $\textit{done}$, not in $\textit{blocked}$, and every predecessor in $\textit{done}$. What makes the last condition safe: a failed task's whole cone is in $\textit{blocked}$, so no sub-task is ever dispatched on a failure report |
| $\text{Pick}(\cdot)$ | the prioritization rule — which of the ready sub-tasks is executed first when workers are limited |
| $\textit{blocked}$ | the sub-tasks that can no longer be executed — the cones of everything that failed. It is returned, not resolved here |
| $\textit{onfail}$ | the blocking policy: $\textit{stop}$ returns as soon as a cone is blocked, $\textit{drain}$ first finishes whatever is still ready. A parameter, because neither is right in general |
| $\operatorname{dom}(\textit{out})$ | the domain of $\textit{out}$ — the set of all task nodes $\tau$ that have already been attempted and have recorded outputs |
$$ \begin{array}{l} \textbf{Schedule}(G = (T, E),\ \textit{out},\ \textit{onfail}) \\ \hline \textbf{Input: } \text{a plan DAG } G \text{ with sub-tasks } T \text{ and dependency edges } E \text{; the store } \textit{out} \text{ so far; a policy } \textit{onfail} \in \{\textit{stop}, \textit{drain}\} \\ \textbf{Output: } \text{the updated } \textit{out} \text{, and the tasks left blocked} \\ \hline 1: \quad \textit{done} \gets \operatorname{dom}(\textit{out}) ;\quad \textit{blocked} \gets \bigcup \{\, \operatorname{desc}(\tau) \mid \tau \in \textit{done},\ \textit{out}[\tau] \text{ is not a verified result} \,\} \\ 2: \quad \textbf{while } \big(\textit{ready} \gets \text{ReadySet}(G,\ \textit{done},\ \textit{blocked})\big) \neq \emptyset \textbf{ do} \\ 3: \quad \qquad \tau \gets \text{Pick}(\textit{ready}) \qquad \text{// prioritization; the whole ready set may be dispatched at once} \\ 4: \quad \qquad r_\tau \gets \text{Solve}_\tau\big(\tau,\ \{\, \textit{out}[u] \mid u \in \operatorname{pred}(\tau) \,\}\big) \qquad \text{// the inputs that flow along the edges} \\ 5: \quad \qquad \textit{out}[\tau] \gets r_\tau ;\quad \textit{done} \gets \textit{done} \cup \{\tau\} \qquad \text{// ATTEMPTED — an artifact, or a failure report} \\ 6: \quad \qquad \textbf{if } r_\tau \text{ is not a verified result } \textbf{then} \qquad \text{// a hard failure OR a negative verdict — the scheduler does not distinguish} \\ 7: \quad \qquad \qquad \textit{blocked} \gets \textit{blocked} \cup \operatorname{desc}(\tau) \qquad \text{// nothing downstream can run on an input that never arrived} \\ 8: \quad \qquad \qquad \textbf{if } \textit{onfail} = \textit{stop} \textbf{ then return } \textit{out},\ \textit{blocked} \\ 9: \quad \textbf{return } \textit{out},\ \textit{blocked} \qquad \text{// the caller reads } \textit{out}[\tau] \text{ to see WHICH it was, and repairs} \\ \hline \end{array} $$
The scheduler detects and reports; it does not repair. Both kinds of failure take the same branch here, because the difference between them is not a difference in what a dispatcher can do about them — it is a difference in what the caller does next, and the caller reads $\textit{out}[\tau]$ to tell them apart. This is the static plan vs. dynamic execution graph distinction made operational: the retry cycle lives in the control loop, not in the DAG being topologically sorted, so the node that inserts a retry is the replanner, not this loop.
Line 1 is what makes the box re-entrant. $\textit{blocked}$ is derived from the store rather than reset, so a cone blocked in an earlier round stays blocked when the scheduler is called again on a repaired graph — and a repair node, being fresh, is in nobody's cone.
Line 8 is a policy, not a default. Under $\textit{stop}$ the scheduler returns the moment a cone is blocked; under $\textit{drain}$ it re-enters the loop and finishes everything still ready, returning once nothing is left to run.
(Line 2 recomputes the ready set as its own loop condition: a completed sub-task may unblock its successors.)
run_tests comes back red. That is a verdict — the step ran and produced exactly the signal
the plan exists to obtain. report is blocked for as long as the failure stands — and the replanner then lifts it, by
appending fresh nodes and re-pointing report onto them:
write_code' is not write_code run again. Its input is larger: the spec, the data, and the
failing test report. That is why the retry can succeed where the original failed — and why a blind
re-run usually cannot.write_code'),
the test is wrong (write_tests'), or the data is not what the spec assumed (fetch_data' —
and then everything downstream is re-derived). Choosing wrongly burns an attempt.The cheapest-looking repair is to leave the failed attempt in the context and append "that was wrong, try again". It is also the one that loops, and the mechanism has a name: context poisoning — "when a hallucination or other error makes it into the context, where it is repeatedly referenced" (Breunig, 2025). The wrong result does not stay an attempt; at every later turn it is re-read as established fact.
Two measurements say why this is structural rather than a matter of prompting harder:
Telling a poisoned context that it made a mistake does not remove the poison — it adds a turn.
What works instead is structural — change what the retry is allowed to read.
| Strategy | Mechanism | What it costs |
|---|---|---|
| Fresh-context retry | re-run the step in a clean context: the goal, the inputs, and a distilled failure report — not the failed transcript | an extra call, and something must write the report |
| Checkpoint rollback | reset to the last node whose result was verified and re-derive from there | re-running everything below the checkpoint |
| Deterministic fallback | after $\kappa$ attempts stop asking the model: run a hard-coded script, return a partial result, take the safe branch | a worse answer, reliably |
| Escalation | hand the step to a human with the failure attached (HITL) | latency, and a person's attention |
The first is the containment argument of a sub-agent boundary: a step that runs in its own window returns "a condensed, distilled summary of its work" rather than everything it read (Rajasekaran et al., 2025), so what a poisoned attempt can contaminate is bounded by what it may hand back.
The rule. Repair by rebuilding the context, not by appending to it. A retry that reads the failure but not the failed reasoning is a new node with a better input; a retry that reads the whole transcript is the same node with a longer one.
Most plans are built from a few reusable shapes — all DAG motifs — and each has a characteristic way of going wrong. Knowing which one you built tells you what to check for.
| Pattern | Shape | Example | Primary failure mode |
|---|---|---|---|
| Chain (prompt chaining) | $\tau_1 \to \tau_2 \to \tau_3$ | draft → edit → format | cascading error — $p^{\,n}$, unchecked |
| Fan-out (map) | one source → many independent tasks | summarise each of 20 files | a partially failed batch — some branches return, some do not, and the aggregator is not told which |
| Fan-in (reduce) | many tasks → one aggregator | combine the 20 summaries | schema drift at the aggregator |
| Router / branch | pick one sub-plan by a condition | "code question or research question?" | misrouting — and the branch not taken is never revisited |
| Hierarchy | a task expands into a sub-plan | a sub-goal handed to a specialist | a stale plan — the node observes what the plan did not anticipate |
The first three go wrong inside a step or at its boundary — a per-step verifier and a validated contract catch them. The last two are structural: the step succeeded and the plan was wrong about it, so detection there must compare the observation against the plan, not against the step's own output.
(Fan-in has a primary of its own: Besta et al. (2024) make aggregation — a node of in-degree > 1 merging sub-results — the defining operation of Graph-of-Thoughts,.)
A chain is a fixed pipeline. Routing adds conditional logic: classify the input or the state, and dispatch to the right branch — a specialised sub-agent, a tool, or another chain (Gulli, 2025).
| Mechanism | How | Trade-off |
|---|---|---|
| LLM-based | prompt the model to output a category label | flexible, handles nuance; costs a model call |
| Embedding / semantic | embed the query, route to the nearest route embedding (cosine) | meaning-based, no keywords (→ L-Embed) |
| Rule-based | if / switch on keywords, patterns, structured fields |
fast and deterministic; brittle to novel input |
| ML classifier | a fine-tuned classifier — the routing logic lives in weights | robust at scale; needs labelled data |
Routing applies at the start (classify the task), mid-chain (pick the next step), or to choose a tool. In a state machine it is a conditional transition (→ L-Orch); with role-agents it is the supervisor.
Where the whole apparatus is actually used, where it stops paying, and how to choose.
Decomposition matters most in long-horizon, verifiable domains. Both canonical ones are assembled the same way: a macro-planner fixes the shape of the work, and short-horizon ReAct executors run inside its nodes.
The coding agent — edit · run · observe. $$ \text{edit code} \;\to\; \text{run tests} \;\to\; \text{observe output \& errors} \;\to\; \text{fix} \;\to\; \dots $$
The web agent — navigate · read · extract · act.
Read down the questions; the first yes decides.
| Question | If yes → | Why |
|---|---|---|
| Is the "how" known and repeatable? | a fixed workflow in code | structural reliability by construction; nothing to hallucinate |
| Is the environment unknown until observed? | ReAct, or a hybrid graph with agentic nodes | an upfront planner would have to enumerate what it cannot observe |
| Do you need the plan inspectable, approvable, or resumable? | a plan object — plan-and-solve | only a planner call before any action leaves an artifact outside the model's context |
| Are the steps independent and is latency the constraint? | an explicit DAG + ready-set dispatch (LLMCompiler-shaped) | the critical path replaces the sum of the durations |
| Are the steps dependent, the environment known, and tokens the constraint? | ReWOO-shaped: one plan, variable bindings, no replanning | removes the $k\,\Theta(C)$ and $(k-j)$ terms |
| Does the plan go stale during the run? | replanning — coarse (regenerate the remainder) or fine (insert · prune · re-edge) | decomposition-first is incomplete without a path back to the planner |
And, orthogonally to all of it: put a check on every boundary you create, decide what that check reads, and give it a retry budget. A boundary without a verifier buys context and parallelism — but not reliability.
depends_on
fields are the edges — validate it (unknown prerequisites, cycles, unknown tools) and hand it to
the scheduler from Part A. Then build a small web/coding agent (your agent from → L-ReAct + Playwright) that
decomposes a task just-in-time instead, browses/reads or reads/runs code, and produces an
answer. The two halves are the lecture's static vs. dynamic pair, run end to end.Part I — boundaries and checks
Part II — the plan as an object
Part III — scheduling and recovery
Next week: searching over decompositions — Tree-of-Thoughts.
Decomposition, verification and reflection
w/o Tool ablation and the generation–discrimination–critique gap.Planning: strategies and architectures
#E1); the input-token arithmetic and its measurement (64 % fewer tokens at +4.4 accuracy); the tool-failure asymmetry; and §4's "enumerate all possible plans" limit on upfront planning.Graphs, scheduling and context
Domains and benchmarks