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:
Task decomposition divides a task into steps, places a check on each boundary between two steps, and represents the plan as an object that the harness — the runtime around the model — schedules and repairs. Part I derives what a boundary with a check is worth, Part II states how three planning architectures construct the plan, Part III how a runtime orders, parallelises and repairs the plan as a dependency graph, and Part IV where decomposition is applied and how an architecture is selected:
A chain of dependent steps succeeds only if every step succeeds, so its success probability falls exponentially with the number of steps: task decomposition places a boundary between two steps, and the boundary raises the success probability only if a check of the step's result is placed on it.
The reliability of a chain is the probability that all of its $n$ dependent steps are correct; it is the product of the conditional reliabilities of the steps and, when every step is correct with the same probability $p$, falls exponentially with $n$.
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 support decomposing a task into sub-tasks — higher reliability, a smaller context per step, and independent steps that run in parallel or are reused — and only the first depends on a check at the boundary between steps.
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.
A monolithic prompt solves all $n$ steps of a task in one generation; a prompt chain issues one model call per step and passes each call the outputs of the earlier calls. The chain raises the per-step success rate only if a step solved in a call of its own succeeds more often than the same step inside the long generation, $p > p_{\text{eff}}$ — an empirical question that the split alone does not settle.
The per-step rate of a monolithic prompt. One generation yields one verdict on the whole task; the outcome of a single step is not observed. The per-step rate $p_{\text{eff}}$ is therefore defined from the task success, as the geometric mean of the conditional step rates:
$$ p_{\text{eff}} \;:=\; P(X_{1:n}{=}1)^{1/n} \;=\; \Big( \prod_{i=1}^{n} P(X_i{=}1 \mid X_{1:i-1}{=}1) \Big)^{1/n}. $$
The definition makes $P(X_{1:n}{=}1) = p_{\text{eff}}^{\,n}$ true for every task; it says nothing about how large $p_{\text{eff}}$ is.
A small difference per step becomes a large difference per task, because the task success is the $n$-th power of the per-step rate. At $n = 10$, a per-step rate of $0.8$ instead of $0.9$ (11 % lower) lowers the task success from $0.9^{10} \approx 0.35$ to $0.8^{10} \approx 0.11$ (69 % lower, a factor of $(0.9/0.8)^{10} \approx 3.2$).
Evidence. Zhou et al. (2023) introduce least-to-most prompting, a prompt chain:
"The key idea in this strategy is to break down a complex problem into a series of simpler subproblems and then solve them in sequence." — Zhou et al. (2023), abstract
The task is last-letter concatenation: given a list of $L$ words, output the string of their last letters ("thinking, machine" → "ge"). The monolithic condition is one chain-of-thought generation for the whole list. The chain condition issues one call per prefix of the list — the first word, the first two words, …, all $L$ words — and each call receives the answers to the shorter prefixes and adds the last letter of one word. Measured is the share of 500 random lists per length that the model (code-davinci-002) answers correctly (§3.1, Tab. 4); the per-step rate is computed here as $\text{accuracy}^{1/L}$, one step per word:
| List length $L$ | Monolithic: accuracy · $p_{\text{eff}}$ | Chain: accuracy · $p$ |
|---|---|---|
| 4 | 84.2 % · 0.958 | 94.0 % · 0.985 |
| 12 | 31.8 % · 0.909 | 74.0 % · 0.975 |
The per-step rate of the single generation falls as the generation grows; the per-step rate of the chain stays near 0.98. The two conditions differ in more than the split: the chain's exemplars show how an answer is built from the answer to the shorter prefix, whereas the chain-of-thought exemplars build every answer from the start. The gap therefore measures the method as a whole, not the split alone.
The design pattern. Gulli (2025, Ch. 1) names the chain the prompt chaining pattern (also called the pipeline pattern) and lists the failure modes of a monolithic prompt that it addresses: instruction neglect (parts of the instruction are ignored without notice) · contextual drift (the initial context is lost) · error propagation (an early error is carried into every later part) · context-window pressure · hallucination under load.
Example (Gulli, 2025, Ch. 1). "Analyse this market-research report, summarise it, extract trend data points, and draft an email" — the model may produce a good summary and still fail at 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 check between calls.
Verification is a check placed after a step that decides whether the step's result is correct; a result that the check rejects is produced again before the next step uses it. The check is a program (unit tests, a schema validator), a language model acting as a critic, or a person (see Kinds of Check).
Result. A check raises the reliability of a step only if it rejects faulty results more often than correct ones. Deterministic checks of a formal artifact meet this condition. A language model used as a critic of programs often does not rank correct and incorrect programs reliably enough for a loop of revisions to beat choosing the highest-scored of several candidates; it comes closer once it is given the result of executing the program.
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}$. An error the check does not detect, a fraction $1-d$ of the faulty results, is passed on at once: with any number of retries the step fails with probability at least $(1-p)(1-d)$.
A step that is checked after every attempt and retried until the check passes needs a random number of attempts $N$; its reliability and its cost follow from one geometric series. The formula $p'$ rests on two assumptions: the check never rejects a correct step, so $d$ is a detection rate, and a retry is a fresh attempt with the same success probability $p$, since detecting an error does not repair it. An attempt is faulty and detected with probability $(1-p)\,d$, and only then is the step attempted again:
$$ p'_\infty \;=\; \sum_{j \ge 0} \big[(1-p)\,d\big]^{\,j}\, p \;=\; \frac{p}{1-d\,(1-p)} , $$
which is $1$ for $d = 1$ and $p$ for $d = 0$, the perfect-check and no-check rows of the table.
Cost. $N \ge 1$ counts the attempts, the first one included, so $N-1$ is the number of retries. The step needs more than $j$ attempts with probability $P(N > j) = [(1-p)\,d]^{\,j}$, so
$$ \mathbb{E}[N] \;=\; \sum_{j \ge 0} P(N > j) \;=\; \frac{1}{1-d\,(1-p)}, \qquad p'_\infty \;=\; p \cdot \mathbb{E}[N] . $$
Reliability rises in proportion to the number of model calls. At $p = 0.9$ and $d = 1$, $\mathbb{E}[N] = 1.11$: 11 % more model calls raise the success probability of a ten-step task from $0.35$ to $1$. At $p = 0.5$ the same perfect check costs $\mathbb{E}[N] = 2$ attempts per step.
A check that also rejects correct results can lower the reliability of a step. The formula $p'$ of the one-retry case 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).$$
Condition. The check raises the reliability of the step exactly when $d > \phi$, that is, when it detects faulty steps more often than it rejects correct ones.
An ungrounded critic, one that reads only the model's own draft, has no evidence that separates a correct draft from a faulty one, so nothing ensures $d > \phi$.
Evidence. Shinn et al. (2023, §4.3, Tab. 3) measure this case with Reflexion, a retry loop for code generation: the model writes a program, runs up to six unit tests that it has generated itself, and after a failed test writes a short explanation of the failure that the next attempt reads; the run stops as soon as all tests pass. On the 50 hardest problems of HumanEval-Rust — the hand-written Python programming problems of HumanEval, each checked by unit tests (Chen et al., 2021), translated to Rust with the MultiPL-E compilers (Cassano et al., 2023) — GPT-4 solves the following share of problems with its final program (pass@1):
| Variant | Self-generated tests | Problems solved |
|---|---|---|
| GPT-4, no loop | — | 60 % |
| Reflexion | yes | 68 % |
| Reflexion without the tests | no | 52 % |
Without the tests the loop has no signal that a program is already correct. It therefore runs every iteration and revises correct programs as well; the authors read the 8-percentage-point drop below the no-loop baseline as the effect of these "harmful edits to the implementation".
Design rule. A retry loop needs a check that can say "done"; without one, every iteration is a chance to break a correct result.
A check is classified by what decides its verdict: a program, a language model, or a person. Pan et al. (2024, §2) separate self-feedback, in which the model judges its own output, from external feedback — tools, other models, knowledge sources, metrics. The three kinds differ in what they can check, in their rates $d$ and $\phi$, and in their cost:
| Kind | What decides | Examples | $d$ and $\phi$ | Cost per check |
|---|---|---|---|---|
| deterministic check | a program that executes or validates the artifact; the same artifact always receives the same verdict | unit tests, compiler, type checker, JSON-Schema validator | $\phi \approx 0$ if the tests are correct; $d$ is bounded by what the tests cover | milliseconds to seconds |
| model critic (LLM-as-a-judge) | a language model that scores the artifact against criteria — the same model (self-critique) or another one | a rubric score, a pairwise preference, a "Is this program correct?" verdict | both must be measured on the task; known biases toward the position, length and source of an answer | one model call |
| human in the loop | a person who approves, rejects or corrects | review of a pull request, approval before an irreversible action | the reference against which the other two are measured; varies between reviewers | minutes to days |
Evidence: a deterministic check is only as good as its tests. In Reflexion's runs, of the programs that pass the unit tests the model wrote itself, 1.4 % fail the benchmark's own tests on HumanEval and 16.3 % on MBPP (Shinn et al., 2023, §4.3). Each such program is a fault the check did not detect: the tests do not cover it.
Evidence: a model critic is measured against people. On open-ended answers, GPT-4 as a judge agrees with human experts on 85 % of pairwise comparisons; human experts agree with each other on 81 % (Zheng et al., 2023).
Design rule. Use a deterministic check wherever the artifact is formal (code, a query, a JSON object); a model critic where no such check exists, with its accuracy measured on the task; and a human at boundaries where an error is irreversible or expensive. A model critic that receives the result of a deterministic check combines the first two (see Grounding of the Critique).
The quality of a critic is measured by its discrimination accuracy. Re-ranking gains from every increase in that accuracy; tree search beats re-ranking only from 90 %, iterative correction only with a perfect critic, and the language-model critics tested on text-to-SQL reach 49–77 %. A generator (also called producer) is the language model that writes the candidate programs; a critic is a model that assigns each candidate program a correctness score. Chen, Ziru, et al. (2024) keep the same generator, CodeLlama-13B, in every experiment and compare three ways of using the critic:
| Method | Procedure |
|---|---|
| re-ranking | the generator writes five complete programs; the one the critic scores highest is returned |
| iterative correction | the generator writes five programs and the critic scores each; the generator then revises the highest-scored program, and the critic scores the revisions — up to ten rounds, stopping early once a score exceeds 0.99 or has not improved for three rounds |
| tree search | Monte-Carlo tree search over partial programs: each partial program is completed by sampling, and the critic scores the completed program |
Measured quantity. The critic is evaluated by its discrimination accuracy: over pairs of one correct and one incorrect program for the same task, the share of pairs in which the correct program receives the higher score. For a critic that only passes or flags, a pair is ranked correctly exactly when the correct program passes and the incorrect one is flagged, so the accuracy equals $(1-\phi)\,d$. It is high only when $d$ is high and $\phi$ is low.
Evidence: the accuracy each method needs. The authors replace the critic by a simulated one that returns the true score with probability $\tau$ and the inverted score otherwise, so $\tau$ bounds its accuracy. Tree search solves fewer tasks than re-ranking for $\tau \le 0.8$ and overtakes it only from $\tau = 0.9$; iterative correction does not differ from re-ranking below $\tau = 1.0$ (Chen, Ziru, et al., 2024, Fig. 3).
A verifier — the check of a step — measures the correctness of the step only when it examines the artifact the step produced, not the model's account of how it was produced. A step of a language model yields two outputs:
A verifier that grades the narration measures whether the explanation is plausible. That is a different quantity, because the narration need not contain what determined the artifact: a reasoning text can support an answer that a bias in the prompt caused, without mentioning the bias (Turpin et al., 2023).
Design rule. A verifier checks the artifact. The narration is used to diagnose a failure and is never taken as the record of what determined the answer.
| 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 is deterministic: it compares the artifact with a reference that the model's output does not contain — a test, a schema, a database, the source text — and returns a verdict the harness can branch on.
A robust check decides only a formal property of the artifact: whether the tests pass, the object matches the schema, the query runs or the quoted span occurs in the source. Whether the artifact does what the sub-task asks is a semantic property; a deterministic check decides it only where a reference for it exists, and otherwise a model critic or a person checks it (see Kinds of Check).
Reflection is a loop in which an output is evaluated and the evaluation is used to revise the output, until the output passes or a retry limit is reached. Gulli (2025, Ch. 4) defines the pattern:
"The Reflection pattern involves an agent evaluating its own work, output, or internal state and using that evaluation to improve its performance or refine its response." — Gulli (2025, Ch. 4)
Reflection is the check-and-retry of Step Reliability with Verification with one change: the retry receives the critique, the result of the evaluation, instead of starting again from the same input. Pan et al. (2024, §2) call the loop post-hoc correction: the finished output is revised, and the parameters of the model are not changed. Reflexion (Shinn et al., 2023) is one published construction of the loop, not a second name for it.
The loop runs at a step boundary, before the step's output is passed downstream:
The detection rate $d$ of the loop is the detection rate of its evaluation step. It depends on what the evaluation reads: the artifact alone, or also the result of executing it (see Grounding of the Critique).
The producer–critic pattern assigns the reflection loop to two roles: a producer, the generator that performs the task, and a critic that evaluates the result. Each role is a separate call with its own prompt; the two calls may use the same model or two different models (also generator–critic, producer–reviewer; Gulli, 2025, Ch. 4):
| Role | Its job | How it is realised |
|---|---|---|
| Producer (generator) | 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, with its own system prompt and often a separate model |
Gulli (2025, Ch. 4) gives the reason: "While a single agent can perform self-reflection, using two specialized agents (or two separate LLM calls with distinct system prompts) often yields more robust and unbiased results."
Evidence: a different model as critic. Chen, Lingjiao, et al. (2025) build a reflection loop with three modules — generator, critic, refiner — and assign a model to each module. The metric is accuracy, the share of problems solved. On LiveCodeBench, a benchmark of programming problems, the best single model for all three modules is Claude 3.5 Sonnet with 89 %. 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." With Claude 3.5 Sonnet as generator and refiner and GPT-4o as critic, the accuracy is 95 %.
Design rule. The critic is a separate call; where a second model is available, it is tested as the critic first.
A critique is grounded when the critic reads evidence that the producer did not generate — a test result, a tool output — and ungrounded when the critic reads only the task and the producer's output. Three published constructions of the reflection loop assign the critic role in different ways and give the critic different evidence:
| Construction | Critic | Evidence the critic reads | Measured without that evidence |
|---|---|---|---|
| Self-Refine (Madaan et al., 2023) | the producing model, under a feedback prompt | none: the task and the output | on GSM8K, grade-school maths problems, the feedback is "everything looks good" for 94 % of instances; the solve rate moves from 74.8 % to 75.0 % (ChatGPT) and from 92.9 % to 93.1 % (GPT-4) |
| Reflexion (Shinn et al., 2023) | two calls: an Evaluator returns the verdict, a self-reflection call writes the critique; the producer (Actor) repeats the attempt with the critique in its context | the result of unit tests that the model generated, or a heuristic; on HotpotQA, a multi-hop question-answering benchmark, exact match against the reference answer — available only under evaluation, not in deployment | on the 50 hardest HumanEval-Rust problems, GPT-4 solves 52 % of the problems with the unit tests removed, against 60 % for a single attempt without reflection and 68 % with the tests. |
| CRITIC (Gou et al., 2024) | the producing model, under a verification prompt | the output of a tool it calls — search engine, Python interpreter, toxicity API | with the tool removed, the model still critiques and revises: average F1 over three question-answering benchmarks −0.03 and +2.33 for the two models tested, +5.6 and +7.7 with the tool; solve rate on three maths benchmarks −3.3 to +20.3 percentage points against the initial program, at least as high with the tool as without it in every case |
A critic that reads its evidence never ends below the same loop without that evidence; without it, the effect of the loop ranges from a loss to a gain. In the Reflexion row, the results of the unit tests are the grounding of the critique. The loop without the tests scores below a single attempt: it has no signal that a program is already correct, so it revises correct programs as well (see Step Reliability with Verification); with the test results the same loop raises the share of solved problems from 60 % to 68 %. In the CRITIC row, the tool output (search results, the output of the Python interpreter) is the grounding of the critique: without it the critique lowers some results and raises others; with it no result is lower than without it.
Evaluator, Actor and CRITIC are the papers' own names. CRITIC is the name of a method, not the critic role: in CRITIC the producing model takes the critic role, and the tool supplies the evidence it reads. All three constructions are instances of the producer–critic pattern; only the evidence of the critic separates the grounded from the ungrounded ones.
Gou et al. (2024) state the conclusion:
"Exclusive reliance on self-correction without external feedback may yield modest improvements or even deteriorate performance." — Gou et al. (2024)
Mechanism. Gou et al. attribute the result to the generation–discrimination–critique gap: a model that can generate a correct answer cannot reliably decide whether the answer it generated is correct.
Design rule. A reflection loop is designed from the critic's evidence: the loop is added only where the critic can read a test result, a tool output or a schema that the deployed system has — never the answer key of a benchmark.
A repair loop that ends worse than re-ranking fails in one of two ways, and only one of them is repaired by more attempts. Re-ranking samples several candidates and returns the one the critic scores highest. Chen, Ziru, et al. (2024, Tab. 6) classify the cases in which re-ranking finds the correct program and iterative correction or tree search does not, on Spider and Bird, two text-to-SQL benchmarks, and on GSM8K, each with the strongest critic of the task — 9 to 48 cases per combination of benchmark and method:
| What happened | Repaired by | Share of these cases | |
|---|---|---|---|
| discrimination error | the critic scored an incorrect candidate above a correct one | a better critic only — no further round, budget or search rule | 50–78 % in each of the six combinations (Spider, iterative correction: 29 of 37) |
| exploration error | the correct candidate was never generated before the loop stopped | more rounds, more diverse sampling | the remainder |
In every combination, discrimination errors are at least half of the losses, and a larger budget does not repair them.
Evidence: a correct answer overwritten. On Spider, in 15 of iterative correction's 29 discrimination errors the correct program had already been generated in an earlier round. The critic scored a later, incorrect revision higher, so the early exit was never triggered and the correct program was replaced.
Design rule. The loop keeps the best-scored candidate of all rounds outside the loop and returns it. A later round can then lower the result only if the critic scores an incorrect candidate above every earlier one.
Structured output is a step output in a fixed, machine-readable format, typically a JSON object that conforms to a JSON Schema, a specification of the required fields and their types. At the boundary between two steps of a decomposed task, it lets the harness check by program the object that the next step reads. Such a boundary lies between a step and the step that reads its result, and between the planner and the runtime that parses its plan into a dependency graph (see Part II — The Plan as an Object). The schema is the contract between the two steps: the format that the producing step must deliver and on which the reading step relies. Free text has no format that a program can check: if the next step misreads a free-text output, no check detects the error, and the next step continues from a wrong input (Gulli, 2025, Ch. 1).
The output of a trend-extraction step as a JSON object (Gulli's example; the figures are his illustration):
{
"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."}
]
}
Two mechanisms ensure that a step's output conforms to its schema, and they act at different times:
| Mechanism | When it acts | What it guarantees |
|---|---|---|
| schema validation | after generation: the harness parses the object and checks it against the schema; a failed check triggers a retry | an invalid object is not passed to the next step |
| constrained decoding | during generation: every token that cannot continue a valid object receives probability zero (Willard & Louf, 2023) | an invalid object is not generated |
The arguments of a tool call are checked against the tool's schema in the same way.
What a schema check detects. A schema validator is a deterministic check (see Step Reliability with Verification) of a formal property of the object (see Robust Checks, by Domain). With a correct schema it never rejects a well-formed object, so $\phi = 0$. It detects unparseable output, missing fields and wrong types, and nothing else: a well-formed object with a false value, such as an invented figure, passes it. Its detection rate covers format faults only.
Design rule. Every step boundary at which a program reads the output (a parser, the scheduler, the arguments of a tool call, a step that reads named fields) carries a schema, and the harness validates the object before the next step reads it. An output that a person reads next, such as the final answer, needs no schema: wrapping it in an object checks only the wrapper. A check of the content is added wherever the content can be checked.
What a schema adds depends on what performs the step. A step is performed by a tool, by a single model generation or by a ReAct loop, and the three differ in the output they produce without a schema:
| Step performed by | Output without an imposed schema | What a schema adds |
|---|---|---|
| a tool | structured: the tool's interface fixes the return type (a JSON response, a table, an exit code) | little; the harness still checks the status and the error fields of the call |
| a single model generation | free text | a format that a program can check before the next step reads the output |
| a ReAct loop | free text in every turn | a schema for the artifact that the loop returns; its intermediate turns stay free text and do not cross the boundary |
A JSON Schema is one checkable format among several. Code and SQL have the grammar of their language as their format, checked by a parser or a compiler, and their robust check is execution (see Robust Checks, by Domain). A plan is a structured list of sub-tasks with their dependencies, so that the runtime can parse it into a dependency graph.
A plan is the sequence of steps intended to reach the goal: a ReAct loop holds the plan only implicitly in its transcript, whereas Plan-and-Execute, ReWOO and LLMCompiler store it as a data object outside any single generation, so that the harness can inspect, validate and schedule the plan and retry a single step. Huang, Xu, et al. define a plan more narrowly, as the sequence of actions that a model generates for a goal in an environment (Huang, Xu, et al., 2024, §1); a plan in this part names sub-tasks, and an executor turns each sub-task into actions. The plan object also fixes where the check of a step's result sits and what a rejected result triggers: a retry of the step, a new plan, or nothing (see Step Verification).
A decomposed task is planned at two levels: the decomposition strategy splits the goal into sub-tasks, and an executor carries out one sub-task. The strategy, the outer level, decides which sub-tasks exist and in which order they run. The executor, the inner level, performs one of them: a tool call, a single model generation, or a whole ReAct loop.
The executor is the step, not a component between two steps. Between two steps lies the check of Step Reliability with Verification: the executor's result is checked before the next sub-task reads it, and a rejected result sends the sub-task back to its executor. A step in Part I — Boundaries and Checks is anything that succeeds with probability $p$; in this part it is one sub-task carried out by its executor, and the executor determines $p$. The question "which pattern should be used?" has one answer at each level, and the two answers are chosen independently.
The two levels are filled from different menus: what sits outside is a way of splitting a goal; what sits inside is a way of acting. They are the two steps into which Huang, Xu, et al. divide task decomposition: the "decompose" step, which splits the goal, and the "sub-plan step", which plans each part (Huang, Xu, et al., 2024, §3). The survey divides decomposition by when the goal is split. In decomposition-first planning the whole goal is split into sub-tasks before the first action; in interleaved decomposition "each decomposition only reveals one or two sub-tasks at the current state". The two further outer entries lie outside that division: recursive decomposition splits a sub-task only after it has failed, and search is the survey's separate direction multi-plan selection, in which several candidate plans are generated and one is selected (§4).
Outer — how is the goal split?
Inner — how is one sub-task finished?
The two lists are independent menus: any outer strategy can be combined with any inner executor, so a decomposition-first plan may hand one step to a tool call and the next to a ReAct loop that acts.
Choosing an executor: take the most deterministic one the sub-task admits.
In $p^{\,n}$ the outer level fixes the exponent $n$, the number of steps, and the inner level fixes the base $p$, the probability that one step succeeds. The cheapest way to raise a chain's reliability is therefore not a better prompt but a step that cannot be got wrong, which raises $p$ towards $1$ — 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.
A calculator error is a step handed to the wrong executor, not a reasoning failure. 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, and 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 called by the harness, like a subroutine. 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 keeps the outer level's context proportional to the number of steps rather than to the work done inside them. A delegation to a subagent returns an artifact in the same way; it differs in the caller. There, a model decides what to hand over and must state it in a contract — an objective, an output format, guidance on tools and sources, and task boundaries (Hadfield et al., 2025) — while here the harness code fixes the interface.
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 |
|---|---|---|---|
| Decomposition-first, one call per step (a plan object exists) | ✓ | ✓ | ✓ — Plan-and-Execute runs each step with a ReAct agent (LangGraph, 2025) |
| Recursive (decomposition on failure) | ✓ | ✓ | ✓ — ADaPT (Prasad et al., 2024), measured |
| Search (ToT / MCTS) | ✓ | ✓ | ✓ — LATS (Zhou, Andy, et al., 2024), measured |
| Interleaved — 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 trajectory lengthens with every level — the condition under which, for interleaved decomposition, "excessively long trajectories may lead to LLM experiencing hallucinations, deviating from the original goals" (Huang, Xu, et al., 2024, §3.3). 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).
Every pseudocode box this lecture 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; a plan-level retry may add several such nodes) |
| $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{Check}_i(\cdot)$, $v$ | the check of one sub-task's result, and its verdict $v$: it accepts $r_i$ or rejects it with a critique. Like $\text{Solve}_i$, it is chosen per sub-task — a deterministic check where the result is a formal artifact, a model critic otherwise (see Kinds of Check) |
| $\textbf{Checked-Step}_i(\cdot)$ | takes the prompt, as $\text{Solve}_i$ does: $\text{Solve}_i$ followed by $\text{Check}_i$, repeated until the verdict accepts or the retry cap $\kappa$ is reached; it returns $r_i$ or a failure report |
| $\textit{prompt}_i \oplus v$ | the prompt $\textit{prompt}_i$ with the verdict $v$ appended — $\oplus$ is concatenation of text |
| $\text{Render}(\cdot)$ | builds a step's prompt from its sub-task and the results so far — the $\textit{prompt}_i$ that is handed to $\text{Solve}_i$. A plan that states its dependencies lets $\text{Render}$ pass only the results of the predecessors, as Schedule does |
| $\textit{reply}$ | what a replanning call returns: either a Response (finish now) or a new Plan; $\textit{reply}.\textit{steps}$ are its steps and $\textit{reply}.\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.
Decompose-then-solve is the control structure in which the agent first decomposes the goal into sub-tasks and then solves the sub-tasks; every planning architecture of this part is a variation of it. It is decomposition-first planning in its multi-call form — one model call writes the plan, one call solves each sub-task — which is the structure of least-to-most prompting (Zhou et al., 2023): the prompt chain of Monolithic Prompt vs. Prompt Chain with the plan written out. The agent loop of → L-ReAct and this one differ in a single line (1). $\text{Solve}_i$ in the box is the executor of one sub-task; it does not compose the final answer.
$$ \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(\tau_i,\ \textit{results}\big) \qquad \text{// not the goal, not the plan} \\ 5: \quad \qquad r_i \gets \text{Solve}_i(\textit{prompt}_i) \qquad \text{// the executor chosen for this sub-task} \\ 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.
Writing the plan in line 1, before any action, changes how the run terminates, what each step reads and what a failed step can change:
The check of a step's result is harness code, not a decision of the executor: the harness calls a subroutine that solves the sub-task and checks the result, and only a result that the check has accepted enters $\textit{results}$. Decomposition creates the boundary, the result $r_i$ of one call that the next call reads; the check at that boundary raises the reliability of the step from $p$ to $p'$ (see Step Reliability with Verification). The skeleton with the check differs from Decompose-then-Solve in the subroutine that line 5 calls and in line 6, which receives a failure report:
$$ \begin{array}{l} \textbf{Decompose-then-Solve-with-Check}(\textit{goal}) \\ \hline \textbf{Input: } \textit{goal} \text{; the tool set; the retry cap } \kappa \\ \textbf{Output: } \text{the final answer, or a failure report} \\ \hline 1: \quad [\,\tau_1, \dots, \tau_n\,] \gets \text{LLM}_{\text{plan}}(\textit{goal}) \\ 2: \quad \textit{results} \gets [\;] \\ 3: \quad \textbf{for } i = 1 \dots n \textbf{ do} \\ 4: \quad \qquad \textit{prompt}_i \gets \text{Render}\big(\tau_i,\ \textit{results}\big) \\ 5: \quad \qquad r_i \gets \textbf{Checked-Step}_i(\textit{prompt}_i) \qquad \text{// was } \text{Solve}_i(\textit{prompt}_i) \text{: solve AND check} \\ 6: \quad \qquad \textbf{if } r_i \text{ is a failure report } \textbf{then return } r_i \qquad \text{// the plan is static: no repair} \\ 7: \quad \qquad \textit{results}.\text{append}\big((\tau_i,\, r_i)\big) \qquad \text{// only checked results} \\ 8: \quad \textbf{return } \text{LLM}_{\text{answer}}(\textit{goal},\ \textit{results}) \\ \hline \end{array} $$
$\textbf{Checked-Step}_i$ is the subroutine of line 5. It takes the prompt, as $\text{Solve}_i$ does, calls the executor $\text{Solve}_i$, and checks the result:
$$ \begin{array}{l} \textbf{Checked-Step}_i(\textit{prompt}_i) \\ \hline \textbf{Input: } \text{the prompt of sub-task } \tau_i \text{; the retry cap } \kappa \\ \textbf{Output: } \text{a checked result } r_i \text{, or a failure report} \\ \hline 1: \quad \textit{prompt} \gets \textit{prompt}_i \\ 2: \quad \textbf{repeat } \kappa \textbf{ times} \\ 3: \quad \qquad r_i \gets \text{Solve}_i(\textit{prompt}) \\ 4: \quad \qquad v \gets \text{Check}_i(r_i) \qquad \text{// reads the artifact, not the transcript} \\ 5: \quad \qquad \textbf{if } v \text{ accepts } r_i \textbf{ then return } r_i \\ 6: \quad \qquad \textit{prompt} \gets \textit{prompt}_i \oplus v \qquad \text{// the original prompt and the latest critique} \\ 7: \quad \textbf{return } \text{a failure report for } \tau_i \qquad \text{// the cap is reached} \\ \hline \end{array} $$
A planning architecture is fixed by its answers to seven design questions: how the plan is represented, when it is written, by which rule its steps are ordered, who picks the executor of each step, whether the result of a step is checked, when the plan is revised, and who authors its structure. Each question is a place where an architecture departs from the Decompose-then-Solve skeleton, and each architecture of this part is one combination of answers (see Planning Architectures).
| Dimension | Question | Values | Place in the skeleton |
|---|---|---|---|
| Plan representation | What form does the plan take, and how does a step receive earlier results? | none · list · DAG · text document; results by context or by name | output of line 1; line 4 |
| Plan generation time | When is the plan written? | before execution · interleaved · streamed · on failure | line 1 |
| Execution order | By which rule is the next step chosen? | list order · first step of the current plan · ready set (the steps whose inputs are all available) | the loop of line 3 |
| Executor choice | Who picks the executor of a sub-task? | the harness, per sub-task · one fixed executor · the planner | $\text{Solve}_i$ in line 5 |
| Step verification | Is the result of a step checked, and by what? | not at all · by a model · by a program | $\text{Check}_i$ |
| Replanning | When is the plan revised, and how much of it? | never · after every step · on demand · on a failed step | a return from line 5 to line 1 |
| Plan provenance | Who authors the structure of the plan? | the model · code · both | the author of line 1 |
The questions are independent; some answers constrain each other.
The plan representation is the form in which the plan exists during the run, and it decides what the harness can do with the plan. It fixes how the steps and their dependencies are stored.
| Representation | What it stores | What the harness can do with it | Example |
|---|---|---|---|
| none | the transcript of thoughts, actions and observations | nothing between turns: no step can be inspected or retried on its own | ReAct |
| list | the steps, in order | inspect and approve the plan, retry one step, execute in list order | Decompose-then-Solve, Plan-and-Execute |
| DAG (directed acyclic graph) | the steps as nodes, each dependency as an edge | additionally: compute the steps that may run now, run independent steps in parallel, detect a cycle | ReWOO (written as a list), LLMCompiler |
| text document | the steps in prose, for a human reader | approval before execution, a restart after the context is reset; no schedule | the plan files of coding agents: plan mode, ExecPlan |
A dependency is stated by a name in the step, in a separate list, or not at all. A step that
receives earlier results by context finds them in its rendered prompt, and the plan says nothing
about which results it needs. A step that receives them by name carries a placeholder in its argument string (ReWOO's
#E1, LLMCompiler's $1), which a program replaces by the stored result. The placeholder is the
edge: the parser derives the graph from the text of the plan. A separate depends_on list states the
same fact a second time and can contradict the steps it describes.
A plan object need not be a data structure: coding agents store the plan as a Markdown file that the agent writes, a human reads and approves, and a later phase executes. The file is inspected by a person, not by a scheduler. It has no schema, so the harness can compute no ready set from it, and its dependencies are implicit in the order of the steps.
Progress, Surprises & Discoveries, Decision Log and Outcomes & Retrospective (Friel, 2025).| Plan as a data structure (Plan-and-Execute, ReWOO, LLMCompiler) | Plan as a text file (plan mode, ExecPlan) | |
|---|---|---|
| Read by | the harness: scheduler, validator | a human reviewer; a later agent session |
| Dependencies | explicit edges or variable bindings | implicit in the order of the steps |
| What it enables | scheduling, parallel dispatch, the retry of a single step | approval before execution; a restart after a context reset |
Design rule. A plan that a person must approve before execution is written as a text file; a plan whose steps a scheduler dispatches is written as a graph.
Plan generation time is the point in the run at which the plan, or a part of it, is written. Huang, Xu, et al. (2024, §3) divide decomposition by this point into decomposition-first and interleaved decomposition (see Two Levels: Strategy and Executor); two further times occur in published architectures:
| Generation time | When the plan is written | Example |
|---|---|---|
| before execution (decomposition-first) | in full, by one call, before any step runs | Decompose-then-Solve, ReWOO |
| interleaved | one step at a time, from the current state | ReAct |
| streamed | in full, but the first steps run while later ones are still being written | LLMCompiler |
| on failure (recursive) | a sub-task is decomposed only after its executor has failed on it | ADaPT |
Each mode of decomposition has one advantage and one cost, and the cost of each is the advantage of the other (Huang, Xu, et al., 2024, §3.3). Task forgetting is the loss of the original goal in the course of a long run.
| Advantage | Cost | |
|---|---|---|
| Decomposition-first | "a stronger correlation between the sub-tasks and the original tasks, reducing the risk of task forgetting and hallucinations" | "since the sub-tasks are predetermined at the beginning, additional mechanisms for adjustment are required otherwise one error in some step will result in failure" |
| Interleaved | the decomposition is adjusted "based on environmental feedback, improving the fault tolerance" | "for complicated tasks, excessively long trajectories may lead to LLM experiencing hallucinations, deviating from the original goals" |
Two costs apply to both modes: the decomposition itself incurs "additional time and computational costs", and a task "decomposed into dozens of sub-tasks" makes the planning "constrained by the context length of the LLM". The survey states the trade-off as a synthesis of the works it reviews; it does not measure it.
Design rule. A decomposition-first plan is paired with a mechanism that adjusts it — replanning, which the survey files as the refinement step of reflection and refinement (Huang, Xu, et al., 2024, §6); an interleaved run is given a bound on the length of its trajectory.
The execution order is the rule by which the next step is chosen; a scheduler is the runtime component that applies the rule.
| Rule | What decides | Steps in parallel | Example |
|---|---|---|---|
| list order | the loop index | no | Decompose-then-Solve, ReWOO |
| first step of the current plan | the replanner, which rewrites the plan after every step | no | Plan-and-Execute |
| ready set | a scheduler: every step whose inputs are all available may run | yes | LLMCompiler, whose scheduler Kim et al. (2024) call the Task Fetching Unit |
A ready-set scheduler requires a DAG. Whether a step is ready is computed from its dependencies, so a plan that states none can only be executed in list order. A DAG makes parallel execution possible; a scheduler that reads the dependencies carries it out. ReWOO's plan is a DAG, but its Worker executes the steps in the order the planner listed them, so two steps that do not depend on each other run one after the other, although they could run in parallel.
The ready set, its computation by Kahn's algorithm and the bound that the critical path places on parallelism are derived in Topological Order & the Ready Set.
Executor choice is the decision which executor carries out a sub-task: a tool call, a single model generation or a ReAct loop. The dimension records who makes the decision.
| Who chooses | How | Example |
|---|---|---|
| the harness, per sub-task | a policy in code; the boxes write $\text{Solve}_i$ | Decompose-then-Solve |
| nobody: one fixed executor | the same executor runs every step | Plan-and-Execute: one ReAct agent for every step |
| the planner | the plan names the tool of each step; the boxes write $\textit{tool}_s$ and $\operatorname{tool}(\tau)$ | ReWOO, LLMCompiler |
The executor sets the base $p$ of $p^{\,n}$, and the most deterministic executor that a sub-task admits raises $p$ the most (see Two Levels: Strategy and Executor). A fixed ReAct executor gives up this choice for every step. A planner that names the tool turns the choice into model output, which can name a tool that does not exist.
Step verification is the check of a step's result before a later step uses it. Three properties describe it: the point at which the result is read, what decides (a program, a model, or nobody), and what a rejected result triggers. The point at which a result is read decides what a negative decision can repair: one step, the remaining graph, or nothing.
| Architecture | Where a result is read | What reads it, and on which evidence | What a negative decision triggers |
|---|---|---|---|
| Decompose-then-Solve-with-Check | after every attempt of every step | $\text{Check}_i$, chosen per sub-task; it reads the artifact $r_i$ | a retry of that step, up to $\kappa$ attempts |
| Plan-and-Execute | after every step, in the replanning call | a model call that states no criterion of correctness — its prompt asks to "update your plan accordingly" (LangGraph, 2025); a past step is stored as the final message of the ReAct executor, model text and not a tool output | new steps in the plan; a repair is a new step, not a retry of the old one |
| ReWOO | at the end only: the Worker, which executes the tool call of each step, stores each output without reading it | the Solver, the final model call that composes the answer, prompted to use the plans and evidence "with caution" (Xu et al., 2023, §2.1) | nothing: the Solver can discount evidence, but it cannot request new evidence |
| LLMCompiler | once per graph, in the Joiner call, a model call over all results that answers or requests a new plan | a model call over the observations, the outputs of the tools; it decides whether they suffice to answer the query (Kim et al., 2024, App. H), not whether each of them is correct | recompilation: a new plan for the remaining work |
None of the three published architectures checks the result of a step robustly. Their deterministic checks are formal and apply to the plan and the tool calls (the schema of the plan, the dependencies parsed from the placeholders, the arguments of a call), while every reading of a result is a model call. A robust check of a result is the designer's addition, made per sub-task as $\textbf{Checked-Step}_i$ with a deterministic $\text{Check}_i$ (see Robust Checks, by Domain).
A check per graph raises the success probability of a chain less than a check per step, because its retry repeats all $n$ steps. The comparison keeps the assumptions of $p'$: one retry, a retry is a fresh attempt with the same $p$, and the check rejects no correct result. With a check after every step, each step ends correct with $p' = p + (1-p)\,d\,p$, and the chain with $(p')^{\,n}$. With one check after the whole graph, $d$ is the probability that the check detects a faulty run; a detected run is executed once more, and if the retry derives all $n$ tasks again it succeeds with $p^{\,n}$:
$$ P_{\text{step}} = \big(p + (1-p)\,d\,p\big)^{n}, \qquad P_{\text{graph}} = p^{\,n} + (1-p^{\,n})\,d\,p^{\,n}. $$
At $p = 0.9$, $n = 10$ and $d = 1$: $P_{\text{step}} = 0.99^{10} \approx 0.90$ and $P_{\text{graph}} \approx 0.35 + 0.65 \cdot 0.35 \approx 0.58$. A per-graph check detects a fault only after every dependent task has consumed it, and the retry is again a chain of $n$ unchecked steps.
Design rule. A sub-task whose result can be checked by program is checked at its own boundary, inside its step. A model call per graph, such as LLMCompiler's Joiner or a replanner, decides what only the set of all results shows: whether the results suffice to answer the goal.
Replanning is the revision of a plan from what the run has observed; in the survey of Huang, Xu, et al. it is the refinement step of the direction reflection and refinement (Huang, Xu, et al., 2024, §6). Two properties describe it: what triggers a revision, and how much of the plan is rewritten.
| Trigger | What is rewritten | Example |
|---|---|---|
| never | nothing: the plan is static | Decompose-then-Solve, ReWOO; Oracle Integration's executor "blindly runs those steps in sequence" (Katta, 2026) |
| after every step | the remaining steps | Plan-and-Execute (LangGraph, 2025) |
| on demand, by a model call over all results | the remaining graph (recompilation) | LLMCompiler |
| on a failed step | the failed sub-task, or the plan from the failed step on | ADaPT, AdaPlanner |
Verification and replanning are the two repairs of one feedback loop: the harness reads a result, decides against a criterion, and repairs. Verification decides whether the result of one step is correct and repairs by retrying that step. Replanning decides what remains to be done and repairs by changing the remaining plan.
The two repairs work in layers. Each architecture can call $\textbf{Checked-Step}$ in place of its executor: a rejected result is first retried within its step, and only a failure report that remains after $\kappa$ attempts reaches the replanner. A replanning call that states no criterion of correctness (Plan-and-Execute), or that decides whether the observations suffice (LLMCompiler), reads a result without verifying it.
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 developer, before the run | by construction: a graph in code cannot be hallucinated | only the branches that were written |
| Hybrid — fixed graph, agentic nodes: a workflow with agentic states | code fixes the graph; the model fills slots and acts inside a node | structure by construction, content by the model | inside a node |
| External planner — the model formalises, a planner plans | a symbolic planner, from the model's formal description of the problem | by construction of the planner, if the formal description is correct | as far as the formal description reaches |
The fourth row is the survey's external planner-aided planning (Huang, Xu, et al., 2024, §5): in LLM+P the model writes the problem in PDDL, the Planning Domain Definition Language, and a classical planner produces the plan (Liu et al., 2023).
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.
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). The same picture holds for navigation: GPT-4 finds a shortest path one cluster away in 0 of 30 runs, and never above 10 % at any temperature; in that evaluation the graph structure contributes more to success than the choice of model, and temperature does not contribute at all (Momennejad et al., 2023). The defect is structural, so a sampling knob does not reach it.
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.
A published planning architecture is one combination of answers to the seven design questions. Five are worked through below:
| Plan representation | Generation time | Execution order | Executor choice | Step verification | Replanning | |
|---|---|---|---|---|---|---|
| Decompose-then-Solve-with-Check (the skeleton) | list | before execution | list order | the harness, per sub-task | $\text{Check}_i$ after every step | never |
| Plan-and-Execute | list | before execution, rewritten after every step | first step of the current plan | one ReAct agent | none; the replanner reads the result without a criterion | after every step |
| ReWOO | DAG written as a list; #E names |
before execution | list order | the planner names the tool | none; the Solver reads all evidence at the end | never |
| LLMCompiler | DAG; $k names |
streamed | ready set, in parallel | the planner names the tool | none; the Joiner decides whether the results suffice | on demand: the remaining graph |
| ADaPT | 3–5 sub-tasks joined by AND or OR | on failure | a recursive controller program | one ReAct executor | the executor's report of success, generated by the model | on a failed step: that sub-task is decomposed |
| AdaPlanner | Python code, one assertion per sub-goal | before execution | the order of the code | the code calls the actions | an assertion on the observation, evaluated by the harness | on a failed assertion: the plan is rewritten and resumed at that sub-goal |
In every row the plan is generated by the model; ADaPT generates it inside a controller program that is fixed in code (see Plan Provenance).
Plan-and-Execute writes a plan, executes one step of it, and then replans the remaining steps from the result of that step (LangGraph, 2025). Position in the design space: a list, rewritten after every step · only its first step is executed · one ReAct agent for every step · no step check · replanning after every step. $$ \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 \textit{reply} \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 } \textit{reply} \text{ is a Response } \textbf{then return } \textit{reply} \\ 9: \quad \qquad [\,\tau_1, \dots, \tau_n\,] \gets \textit{reply}.\textit{steps} \qquad \text{// here } \textit{reply} \text{ is a Plan: "only what still NEEDS to be done"} \\ \hline \end{array} $$
Line 5 passes the executor the current plan, the steps that the replanner has not yet removed, as
context: the plan shows the executor which part of the task is its own and what the later steps will
do. The results so far go only to the replanner (line 7), which is instructed to give each remaining
step all the information it needs; the executor never receives $\textit{results}$ (LangGraph, 2025,
Plan-and-Execute, execute_step and the replanner prompt).
Cost of the design. Each executed step costs one replanning call; in exchange, the plan is revised after every observation. The steps run one after the other: "each task is still executed in sequence, meaning embarrassingly parallel operations all add to the total execution time" (LangGraph, 2025, Plan-and-Execute, Conclusion). The fixed ReAct executor runs every step, whatever the step is, so the choice of the most deterministic executor is given up.
ReWOO (Xu et al., 2023) removes the replanning entirely: the plan is written before any tool
runs, and never revised. Position in the design space: a DAG written as a list, with #E
names · written before execution · list order · the planner names the tool · no step check · no
replanning.
ReWOO adds the following to the shared list under Notation for the Pseudocode; none of them appears in any other box.
| Symbol | Reads |
|---|---|
| $\textit{blueprint}$ | the plan as text — what the planner emits, prior to parsing; ReWOO's "solution blueprint". |
| $\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 \textit{blueprint} \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}(\textit{blueprint}) \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} $$
Difference from the skeleton. A step's arguments are completed by substituting a named value (line 5) instead of re-rendering a prompt from $\textit{results}$; that change turns the plan into a graph. The planner also names the tool of each step, $\textit{tool}_s$.
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"]
Passing results by name removes a 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. HuggingGPT writes its plans in such a format: a task lists its prerequisites in a field dep and reads their outputs as <resource>-k in its arguments (Shen et al., 2023).
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. LLMCompiler removes that limitation.
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 takes its
vocabulary from compilers. Position in the design space: a DAG with $k names · streamed ·
ready set, in parallel · the planner names the tool · no step check · replanning on demand.
The final model call, which either answers or requests a new plan, is not a component in
Kim et al. (2024): the paper implements it as a finish task at the end of every plan.
The name Joiner, used for that call from here on, is LangChain's
(The LangChain Team, 2024).
(from Kim et al., 2024)
1. search("Company A 2024 annual revenue")
2. search("Company B 2024 annual revenue")
3. math("$1 / $2")
4. join()
The Task Fetching Unit replaces the placeholders $1 and $2 of task 3 with the outputs of tasks 1 and 2 before it fetches task 3 to the Executor.
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 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 read off the arguments — the planner emits only numbered calls whose argument strings carry $k placeholders, there is no deps field in the plan text, and the parser constructs the dependency list from them before the scheduler sees a task. So the placeholder is the edge and the input binding at once, which is why the scheduler needs no separate dependency declaration. 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 dispatch half of the scheduler written out under Execution, Failure & Recovery — that box's ready set, substitution and parallel dispatch are the Task Fetching Unit's; its step check $\textbf{Checked-Step}_\tau$, its $\textit{blocked}$ cone, its $\textit{onfail}$ policy and its return of what could not be run are this course's engineering, and have no counterpart in the paper or in its code |
| $\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; the parser reads each task's } \textit{deps} \text{ off its placeholders} \\ 5: \quad \qquad \textit{obs} \gets \text{Schedule}(G,\ \textit{obs}) \qquad \text{// the Task Fetching Unit: Schedule without } \text{Check}_\tau \text{, } \textit{onfail} \text{ and } \textit{blocked} \\ 6: \quad \qquad \textit{reply} \gets \text{LLM}_{\text{join}}(\textit{goal},\ \textit{obs}) \qquad \text{// the JOINER: a Response, or Replan} \\ 7: \quad \qquad \textbf{if } \textit{reply} \text{ is a Response } \textbf{then return } \textit{reply} \\ 8: \quad \qquad \textit{context} \gets \textit{reply}.\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.
LLMCompiler adds two mechanisms to the DAG: the planner streams the graph, and the remaining graph is replanned when it cannot be fixed in advance.
max_replans: 1, so the latency and accuracy figures that follow are single-pass runs
with recompilation switched off (Kim et al., 2024, repository a00c9d3).Measured against a ReAct baseline, LLMCompiler lowers both latency and cost, because the plan removes LLM invocations instead of adding them:
| Benchmark | Latency speedup over ReAct | Accuracy (%), ReAct → LLMCompiler |
|---|---|---|
| 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× over Tree-of-Thoughts | success rate, 74.00 → 75.33 |
Cost falls 3.37× / 6.73× / 4.65× on the three benchmarks, "because it involves less frequent LLM invocations."
ADaPT runs an executor on a task first and decomposes the task only when the executor fails (Prasad et al., 2024). Position in the design space: sub-tasks joined by AND or OR · written on failure · ordered by a recursive controller program · one ReAct executor · the executor's own report of success as the step check · replanning by decomposing the failed sub-task.
Evidence. With GPT-3.5 as executor and planner, success rate on the test splits of three text environments (Prasad et al., 2024, Tables 1–2):
| Environment | ReAct | ReAct, re-run on the whole task (Try Again) | ADaPT |
|---|---|---|---|
| ALFWorld, household tasks ($d_{\max} = 3$) | 43.3 % | 47.8 % | 71.6 % |
| WebShop, online shopping ($d_{\max} = 3$) | 32.0 % | 30.0 % | 44.0 % |
| TextCraft, crafting recipes ($d_{\max} = 4$) | 19.0 % | 15.0 % | 52.0 % |
Try Again calls the executor as often as ADaPT does but never decomposes, so the difference between the last two columns measures decomposition, not repetition.
AdaPlanner writes the plan as Python code in which every sub-goal ends with an assertion on the environment's observation; a failed assertion halts execution, and the model rewrites the plan and resumes it at the failed sub-goal (Sun et al., 2023). Position in the design space: code, one assertion per sub-goal · written before execution · the order of the code · the code calls the actions · a deterministic check of every sub-goal · replanning on a failed assertion.
def solution(agent, start_from=1):
...
assert 'clean' in observation, f'Error in [Step 3]: I cannot clean {found_lettuce} using the sinkbasin 1. {agent.report()}'
ask_LLM()) and the plan continues.start_from.The check is deterministic, its criterion is not. The harness evaluates the assertion, but the model writes it as part of the plan; a missing or wrong assertion is an undetected fault.
Evidence. On the 134 ALFWorld tasks, success rate: AdaPlanner (text-davinci-002) 91.79 %, Reflexion (GPT-3 and GPT-3.5) 88.06 %, ReAct (text-davinci-002) 61.94 % (Sun et al., 2023, Table 2). The assertions, the refinement and the code-style prompt are measured together; the table does not separate the contribution of the check.
A plan whose steps depend on the results of other steps is a directed acyclic graph (DAG): the runtime derives an execution order from the graph, executes in parallel the steps whose dependencies are complete, and repairs the graph when a step fails.
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.
A retry changes the plan only when it crosses a node boundary. The static plan that the scheduler sorts is a DAG: $\tau_1 \to \tau_2 \to \tau_3$. The execution trace contains a cycle, try → fail → retry: $\tau_2 \to \texttt{tests\_failed} \to \tau_2' \to \tau_3$.
The cycle lives in the control loop of the caller, not in the DAG that is sorted topologically.
Graph mutation is the modification of the plan DAG while the plan is executed: nodes are inserted or pruned and dependencies are added or removed, in response to a failed step or to a new observation. Graph mutation is replanning at the granularity of single nodes and edges (see Replanning); Plan-and-Execute and LLMCompiler replan coarsely, by regenerating the remaining steps or the remaining graph.
| 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 |
Mutating a graph that a scheduler is already walking is safe as long as two invariants hold:
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. The check is simpler than the algorithm that produced the order and independent of it; it is a deterministic check of a formal property of the plan (see Robust Checks, by Domain).
$$ \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 } \textit{inDegree}[v] \gets 0 \\ 2: \quad \textbf{for each } (u, v) \in E \textbf{ do } \textit{inDegree}[v] \gets \textit{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 } \textit{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 } v \text{ with } (u, v) \in E \textbf{ do} \\ 10: \quad \qquad \qquad \textit{inDegree}[v] \gets \textit{inDegree}[v] - 1 \\ 11: \quad \qquad \qquad \textbf{if } \textit{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} $$
In a plan, the vertices $V$ are the sub-tasks $T$ and the edges $E$ are the dependencies.
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).
The DAG of a plan determines which sub-tasks may run at the same time, and its critical path bounds how fast the plan can finish. A topological order gives one sequential schedule; two sub-tasks with no path between them are independent and may run in parallel.
The critical path is the path through the DAG with the largest total duration. With unlimited workers, the minimum time to finish the plan, the makespan, is the 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 sub-task $\tau_i$ (not the detection rate $d$ of a check).
Complexity. A longest path in a general graph is NP-hard to find; in a DAG it is found in $\Theta(|T|+|E|)$ time by a topological sort followed by dynamic programming. Critical path, slack and makespan are the terms of 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 language model prompted to answer in JSON, returns one object per sub-task
with a field depends_on that names its predecessors. These fields are the edges, so the parsed plan
is the graph in the figure. The plan states its dependencies in a separate list (see
Plan Representation); a plan with placeholders would derive them from the
arguments of each sub-task. 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 edges, not the durations. The model emits id and depends_on: which
sub-task needs the output of which, a fact it can read from the task statement. It does not emit
dur. A duration depends on the runtime (the latency of a tool, the network, the load of a worker)
and is known only from measurement, such as the latency of earlier runs of the same kind of step
(→ L-Eval). The critical path and the slack are computed from these measured durations.
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())})")
In the example, the critical path is read_spec → fetch_data → write_code → run_tests → report,
with length $1+3+4+1+1 = \mathbf{10}$ time units.
write_tests lies off the critical path and has 5 time units of slack: it may start up to
5 units late, or take up to $2+5=7$ units, before the makespan changes.A spurious edge causes no error; it reduces the slack. fetch_data precedes write_code but not
write_tests, because an edge states what a step needs as input, not what runs first. The code
must match the dataset's columns and quirks, so it needs the data; the tests encode the contract
from the spec, which is known before any data arrive. With the edge fetch_data → write_tests
added, the makespan stays 10, but write_tests is no longer independent and its slack falls from
5 to 2.
When more sub-tasks are ready than workers are free, the scheduler ranks the ready sub-tasks. A topological order states only which orders are legal; the ranking chooses one of them.
Scheduling vs. prioritization. A topological sort answers which sub-tasks may run; prioritization answers which of the ready ones runs first. Prioritization matters only when fewer workers are free than sub-tasks are ready.
Execution runs the sub-tasks of a plan in dependency order; a failure is a sub-task that returns no verified result; recovery changes the plan or the context so that the run can continue. The three responsibilities belong to different components: the scheduler executes and reports failures, and its caller, the planning architecture that handed it the plan, repairs them.
Data flows along the edges. A sub-task $\tau_j$ receives as inputs the outputs of every $\tau_i$ with an edge $\tau_i \to \tau_j$. In a placeholder architecture an edge exists exactly where the planner wrote a reference to $\tau_i$ into the arguments of $\tau_j$: the edge and the variable binding are the same symbol. The scheduler executes the ready sub-tasks, independent ones in parallel, and passes each output to the sub-tasks that depend on it.
A rejected result and a hard failure are both called "failure"; which of the two occurred decides who can repair the step.
| What happened | What the run does with it | |
|---|---|---|
| A rejected result | the step ran and returned a negative result — tests red, the checker rejected the draft | nothing is broken; the check has done its work: the step is retried within $\textbf{Checked-Step}_\tau$ up to $\kappa$ attempts, and a failure report that remains goes to the replanner |
| A hard failure | the step produced no output — the API was down, the file was missing | a transient failure is retried within $\textbf{Checked-Step}_\tau$ up to $\kappa$ attempts; a failure that remains leaves nothing for the successors: the scheduler blocks the descendant cone, and only a new plan can recover it |
At plan level, a retry adds new nodes and revisits none. Feeding the error back changes the input, so the restarted step is a different task instance $\tau'$, added to the graph as a node of its own, and every step derived again from it is added as a new copy as well. The attempts inside $\textbf{Checked-Step}_\tau$ are local to the call and add no node. Without a limit the graph grows without bound; a retry cap $\kappa_{\text{plan}}$ limits how often a run may replan around the same failure. It is the plan-level counterpart of the cap $\kappa$ on the attempts of one step in the reflection loop. The caller sets the cap, because the caller adds the retry nodes.
A scheduler repeats four steps until no sub-task is ready and none is running.
An empty ready set alone does not end the loop: the remaining sub-tasks may wait for one that is still running.
The scheduler detects and reports; it does not repair. It returns the results, the failure reports and the blocked sub-tasks; the caller, the planning architecture, repairs the plan with new nodes and calls the scheduler again.
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 the job of $\textbf{Checked-Step}_\tau$, which returns it when the retry cap $\kappa$ is reached, after retrying a rejected result or a transient hard failure. It has two readers: the replanner, which diagnoses from it, and the join call that composes the answer. A raw transcript here would carry the failed attempt's errors into both (see Context Poisoning and Context Confusion) |
| $\operatorname{dom}(\textit{out})$ | the domain of $\textit{out}$ — the set of task nodes already attempted, each with its artifact or its failure report. A failed node is in it, and is therefore never dispatched again: a retry adds new nodes |
| $\textit{done}$ | the sub-tasks already attempted, whatever they returned; equivalently $\operatorname{dom}(\textit{out})$. |
| $\textit{ready}$ | the ready set: the sub-tasks that may be executed immediately — not yet attempted, not running, not blocked, and with every input available |
| $\textit{running}$ | the sub-tasks dispatched and not yet finished |
| $\text{WaitAny}(\cdot)$ | waits until one of the running sub-tasks finishes and returns it with its result |
| $\text{ReadySet}(\cdot)$ | recomputes it from the run's state: not in $\textit{done}$, not in $\textit{running}$, 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 are dispatched now, as many as workers are free |
| $\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 |
The scheduler neither plans nor repairs. The plan $G$ is an input of the Schedule
procedure, not something it produces. Schedule is the runtime to which a planning architecture
hands its plan: the contents of the $\text{Schedule}$ call that the LLMCompiler box leaves
closed. Replanning and unrolling therefore stay with the caller, and the scheduler returns
what it could not run instead of working around it.
$$ \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 a failure report} \,\} ;\quad \textit{running} \gets \emptyset \\ 2: \quad \textbf{while } \big(\textit{ready} \gets \text{ReadySet}(G,\ \textit{done},\ \textit{blocked},\ \textit{running})\big) \neq \emptyset \;\vee\; \textit{running} \neq \emptyset \textbf{ do} \\ 3: \quad \qquad \textbf{for each } \tau \in \text{Pick}(\textit{ready}) \textbf{ do spawn } \textbf{Checked-Step}_\tau\Big(\text{Render}\big(\tau,\ \{\, \textit{out}[u] \mid u \in \operatorname{pred}(\tau) \,\}\big)\Big) ;\ \textit{running} \gets \textit{running} \cup \{\tau\} \qquad \text{// dispatch, no wait} \\ 4: \quad \qquad (\tau,\ r_\tau) \gets \text{WaitAny}(\textit{running}) ;\quad \textit{running} \gets \textit{running} \setminus \{\tau\} \qquad \text{// the next sub-task to finish} \\ 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 a failure report } \textbf{then} \qquad \text{// a hard failure OR a rejected result — 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} \qquad \text{// running sub-tasks are cancelled; their results are lost} \\ 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 (line 6), because a dispatcher can do the same about either: block the cone. The
difference matters only to the caller, which reads $\textit{out}[\tau]$ to tell the two apart.
The plan $G$ stays acyclic: a retry that crosses a node boundary is not a cycle in $G$ but new nodes
that the replanner adds between two calls of Schedule.
Lines 3–4 dispatch without waiting. Line 3 starts every sub-task that $\text{Pick}$ selects and does not wait for it; line 4 waits for the next sub-task to finish. The loop ends only when no sub-task is ready and none is running: an empty ready set alone does not mean that the plan is finished, because the remaining sub-tasks may wait for one that is still running.
Line 1 lets the caller call Schedule again on a repaired graph. $\textit{blocked}$ is derived from the store rather
than reset: a cone whose failed node was not repaired stays blocked when the scheduler is called again,
while re-pointing the dependents onto a repair node removes them from the cone. A repair node, being
fresh, is in nobody's cone.
Line 8 applies the blocking policy $\textit{onfail}$. Under $\textit{stop}$ the scheduler returns the moment a cone is blocked and cancels the sub-tasks still running: their results are lost, and a later call executes them again. Under $\textit{drain}$ it re-enters the loop and finishes everything still ready, returning once nothing is left to run.
Which node a rejecting verdict concerns. A result that the step's own check rejects is retried within
$\textbf{Checked-Step}_\tau$, and only a failure report that remains reaches line 6. A rejecting verdict of a check node, such as red tests from run_tests, concerns a predecessor: the caller extends the plan with
retry nodes from the predecessor it diagnoses as faulty on (see
Unrolling a Failed Test Run).
(Line 2 recomputes the ready set as its own loop condition: a completed sub-task may unblock its successors.)
A failed test run is a rejecting verdict: the step run_tests executed and produced the signal the plan exists to obtain. report stays blocked while the failure stands. The replanner lifts the block
by adding new nodes and re-pointing report onto them:
The figure is the plan $G'$ after the first replanning round. run_tests (dashed) has failed
and stays in the graph with its failure report in $\textit{out}$. report has been re-pointed: its
edge from run_tests is removed, because a node with a failed predecessor stays blocked, and an
edge from run_tests' is added. A second red run repeats the construction with write_code'' and
run_tests'', until the cap $\kappa_{\text{plan}}$ is reached.
The failing test report enters write_code' through its prompt, not through an edge. The
replanner writes the distilled report into the arguments of write_code'. An edge
run_tests → write_code' would place write_code' in the descendant cone of the failed node, and
line 1 of Schedule, which rebuilds the blocked set from every failure report in
$\textit{out}$ when the replanner calls it again, would block it. run_tests' is a new node as well, because an attempted node
is never dispatched again, and it reads write_tests as run_tests does.
write_code' is a new node, not write_code executed again. Its input is larger: the spec,
the data, and the failing test report. A re-run with the original input receives no
information about the failure.
The choice of the node to unroll is a diagnosis. A failed test means that the code is wrong
(write_code'), the test is wrong (write_tests'), or the data differ from what the spec
assumed (fetch_data', after which every sub-task downstream is derived again). A wrong choice
spends one of the $\kappa_{\text{plan}}$ attempts without repairing the cause.
The cap $\kappa_{\text{plan}}$ is part of the plan, like the step cap of the ReAct loop. It counts
replanning rounds, not tool calls, because each round adds one new copy of write_code
and of run_tests. A loop of textual self-correction is not guaranteed to
converge, that is, to reach a correct result and stop changing it: "the convergence of this textual form of update currently lacks a
guaranteed proof" (Huang, Xu, et al., 2024, §6). Only the cap stops it, or a check that accepts the result.
A failed step leaves content in the run that later model calls read, and one kind of it is not recorded as a failure. Which content a step leaves depends on the kind of failure, a rejected result (the step ran, and the check rejected its result) or a hard failure (the step produced no output; see Execution, Failure & Recovery), and on whether a check detected the fault. The content is written by a model, by the harness, or by the environment, the interpreter, test runner, API or file system on which the executor acts:
| Content | Written by | Kind of failure | Recorded as a failure? |
|---|---|---|---|
| the wrong artifact and the reasoning that produced it, e.g. code that fails the tests | a model: the executor | rejected result | yes: the check rejected it |
| the error message: the failing test report, "connection refused" | the environment | rejected result or hard failure | yes |
| the failure report, a short account of what went wrong, returned when the cap $\kappa$ is reached | the harness ($\textbf{Checked-Step}_\tau$), from the last verdict and the error message | either, after $\kappa$ attempts | yes; as reliable as the verdict of $\text{Check}_i$ |
| an undetected wrong result: a faulty result that no check rejected, because the check misses it or because the step has no check | a model: the executor | none recorded | no: it is stored as a correct result |
An undetected wrong result occurs with probability $(1-p)(1-d)$ per checked step (see Step Reliability with Verification), and with probability $1-p$ per step without a check.
A retry is a further attempt after a failed step, and its input is either rebuilt from the original task or accumulated from the failed attempt. A retry occurs at two levels, and at plan level it may repeat several steps:
write_code' and
run_tests' repeat two steps (see Unrolling a Failed Test Run),
and LLMCompiler's recompilation replaces the whole remaining graph. Restarting at the last verified
node is the checkpoint rollback of Recovery Strategies.A rebuilt input holds the original task and a verdict or a failure report. An accumulated input holds the failed attempt itself: the wrong artifact, the reasoning that produced it and the error messages.
The content of a failed step reaches the next attempt, the replanner, the new nodes and the component that composes the answer, and each of them reads a rebuilt or an accumulated input:
| Reader | Level | What it reads | Input |
|---|---|---|---|
| the next attempt within $\textbf{Checked-Step}_\tau$ | step | the original prompt and the latest verdict, line 6 (see The Step Check in the Skeleton) | rebuilt |
| an executor that runs the check itself as a tool call, e.g. a ReAct executor that calls the tests | step | its own transcript: the failed code, its reasoning and the test report; likewise a conversation to which "that was wrong, try again" is appended | accumulated |
| the replanner | plan | every earlier result, as in Plan-and-Execute, where line 6 appends each $(\tau, r)$ to $\textit{results}$ and line 7 passes all of them to $\text{LLM}_{\text{replan}}$; or only the failure reports that $\textbf{Checked-Step}_\tau$ returns | accumulated, or rebuilt |
| a new node $\tau'$ and the nodes after it | plan | the arguments that the replanner writes; the first new node carries the failure report | rebuilt in form; its content is written by the replanner |
| the component that composes the answer: ReWOO's Solver, LLMCompiler's Joiner | end of the run | all results, a failed one included | accumulated |
Context poisoning is the state in which a hallucination or another error has entered the context and is referenced again at every later turn — "when a hallucination or other error makes it into the context, where it is repeatedly referenced" (Breunig, 2025). Context confusion is the state in which "superfluous content in the context is used by the model to generate a low-quality response" (Breunig, 2025). Which of the two a reader is exposed to depends on the content of a failed step that its input holds (see Outcome of a Failed Step):
| Content in the reader's input | True? | Effect |
|---|---|---|
| an undetected wrong result | no, and stored as correct | context poisoning: every later call reads the error as a fact |
| the wrong artifact of a rejected result | no, and marked as failed | context poisoning only if the reader takes the failed approach up as a basis and repeats it; no cited measurement isolates this effect |
| the error message of the environment, once | yes | the information that the next attempt or the replanner needs |
| the failure report | yes, if the harness assembles it from the verdict of a deterministic check and the error message; not necessarily, if a model critic or a model summary writes it | the information the replanner needs; a report written by a model is a source of context poisoning, shorter than the transcript |
| further copies of the same message, e.g. $\kappa$ times "connection refused" from a tool that is down | yes | context confusion |
A rebuilt input does not remove an undetected wrong result. The replanner reads it as an ordinary result, whether its input is rebuilt or accumulated, and writes it into the arguments of the new nodes, which read it as part of their instruction. A hard failure poisons nothing. Breunig (2025) does not discuss tool errors; the assignment follows from the two definitions.
Superfluous content lowers accuracy, as two measurements show:
The published planning architectures differ in which component reads the result of a failed step, and in whether that component's input is rebuilt or accumulated.
| Architecture | Who reads after a failure | Input | What the accumulated input supplies |
|---|---|---|---|
| Decompose-then-Solve-with-Check | $\textbf{Checked-Step}_\tau$ | rebuilt: the original prompt and the latest verdict | — a generation without side effects starts again from the same prompt |
| Plan-and-Execute | the replanner, in every round | accumulated: the goal, the current plan and every past result, each stored as the final message of the ReAct executor (LangGraph, 2025) | what has been done, from which the replanner keeps "only what still NEEDS to be done" |
| ReWOO | the Solver, once; ReWOO neither retries nor replans | accumulated: all evidence, a failed tool output included (Xu et al., 2023) | the evidence from which the answer is composed |
| LLMCompiler | the planner on recompilation, and the Joiner | accumulated: every earlier plan with its observations and the Joiner's reasoning, prefixed as Previous Plan (Kim et al., 2024, reference implementation) | the plans already tried and their observations; on Game of 24, the states shortlisted in the previous round, from which the new plan starts (Kim et al., 2024, §5.3) |
| ADaPT | the planner | rebuilt: the task alone; the executor "does not get text feedback from prior failures" (Prasad et al., 2024, App. A) | — nothing of the failed attempt informs the sub-tasks; the failure decides only that the task is decomposed |
| AdaPlanner | the refiner | accumulated: the trajectory so far and the plan whose assertion failed (Sun et al., 2023, §2) | the current state of the environment, which the earlier actions have changed, and the failed sub-goal, named by the assertion |
Plan-and-Execute, LLMCompiler and AdaPlanner pass the failed attempt to their replanner, and none of the three separates it into the kinds of content of Context Poisoning and Context Confusion. Plan-and-Execute, ReWOO and LLMCompiler have no step check (see Step Verification), so every result they pass on may be an undetected wrong result. None of the sources measures what the accumulated input costs in poisoning or confusion; the last column states what it supplies to the replanner, read from the prompts and the code.
A recovery strategy is chosen by the content it must keep out of the next call, and a rebuilt input keeps out only what is recorded as a failure.
| Content to keep out | Strategy | Mechanism | What it costs |
|---|---|---|---|
| an undetected wrong result | stronger check | a check per step with a higher detection rate $d$; a deterministic $\text{Check}_i$ where the result is a formal artifact (see Robust Checks, by Domain) | the check, run after every attempt |
| checkpoint rollback | reset to the last node whose result was verified and derive everything below it again; a rollback to a node that passed a weak check restores the fault | re-running every sub-task below the checkpoint | |
| the wrong artifact of a rejected result | fresh-context retry | re-run the failed step, at plan level the steps from the restart node on, with a rebuilt input (see Retry Input): the goal, the inputs and a distilled failure report, not the failed transcript | an extra call, and a component that writes the report |
| repeated copies of an error message | error message once, and caps | the failure report carries the message once; the caps $\kappa$ and $\kappa_{\text{plan}}$ bound the attempts and the replanning rounds | a failure report after $\kappa$ attempts, even where a further attempt would have succeeded |
| a failure that remains after $\kappa_{\text{plan}}$ rounds | deterministic fallback | stop calling the model: run a fixed script, return a partial result, take the safe branch | a lower-quality result, obtained with certainty |
| escalation | hand the step to a person with the failure report attached (human in the loop) | latency, and a person's time |
Why a rebuilt input keeps the failed attempt out. A step executed in a context of its own returns only what it reports. Sub-agents return "a condensed, distilled summary of its work" (Rajasekaran et al., 2025), not everything they read; an error in the failed attempt can therefore reach the next attempt only through the report.
Design rule. A retry reads the failure report, not the failed reasoning: it is a fresh attempt with a better input, not the same attempt with a longer context. At step level, $\textbf{Checked-Step}_\tau$ rebuilds the prompt from the original and the latest critique (line 6). At plan level, the replanner reads the failure reports that $\textbf{Checked-Step}_\tau$ returns, not the earlier attempts, and the retry starts with a new node whose arguments carry the report; the error message thus enters the retry once. The failure report therefore carries what the replanner would otherwise take from an accumulated input: the current state of the environment, where earlier actions have changed it; the evidence for the choice of the node at which the retry starts; and the error message, once. The harness assembles the report from the last verdict and the error message instead of having a model summarize the attempt: a report written by a model, or carrying the verdict of a model critic, is model output and can carry an error to the replanner.
Recovery applies the check-and-retry of a single step to the plan: the check of a step decides that the step failed, and the recovery strategy decides what the retry reads and at which node it starts. The check-and-retry of a single step is derived under Step Reliability with Verification.
| One step | The plan |
|---|---|
| the check $\text{Check}_i$ after one attempt | the verdict tested in line 6 of Schedule: $\textbf{Checked-Step}_\tau$ in line 3, or a check node of the plan |
| the retry cap $\kappa$ on the attempts of one step | the cap $\kappa_{\text{plan}}$ on replanning rounds |
| the critique, read by the next attempt in the reflection loop | the failure report, read by the first retry node $\tau'$ |
| the assumption of $p'$: a retry is a fresh attempt with the same $p$ | a retry with a rebuilt input; with an accumulated input the next attempt reads the failed attempt (see Retry Input), and the assumption does not hold |
| an undetected fault, probability $(1-p)(1-d)$, passed to the next step | an undetected wrong result, read by the replanner and the new nodes as correct (see Outcome of a Failed Step) |
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. AdaPlanner makes this comparison an assertion on the observation that the plan predicted (see AdaPlanner — Assertions and Plan Refinement).
(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.
A developer rarely writes the scheduler: a graph library or a workflow engine supplies it, and the concepts of this part are the settings it is configured with. pydantic-graph, the graph library of Pydantic AI, shows the correspondence:
| Concept of this part | pydantic-graph and Pydantic AI |
|---|---|
| fan-out and fan-in (see Flow Patterns & Their Failure Modes) | a fork, which sends the same input to several steps (broadcast) or one item of an iterable to each (spread), and a join, which waits for all tasks of its fork and combines their results in the order they finish (Pydantic, 2026a) |
| router / branch | a decision node, which chooses the next step by its input (Pydantic, 2026e) |
the ready set, lines 2–4 of Schedule |
the run object GraphRun, which dispatches the tasks of the graph |
| the scheduler detects and reports, the caller repairs | a failing step leaves an ErrorMarker, "allowing the caller to recover" by sending new tasks to the run (Pydantic, 2026b) |
| a retry as new nodes | new tasks sent with override_next (Pydantic, 2026b) |
| the cap on the attempts of a step, the cap on a run | the retries of an agent or a tool; UsageLimits, by default 50 model requests per run (Pydantic, 2026c) |
| the plan as an object with dependencies, without a scheduler | the harness capability Planning: a task list that the model writes and works through itself; it reports the tasks that may start and executes none of them. Its documented purpose: the current plan stays in view without ever entering the instructions, so that an edit of the plan does not invalidate the prompt cache (Pydantic, 2026d) |
| the plan written as a program, the language runtime as the scheduler | the harness capability Dynamic Workflow: the model writes one Python script in which every sub-agent is an async function; independent calls run concurrently where the script uses asyncio.gather, a sub-agent failure raises an exception that aborts the script unless the script catches it, and the run is capped at 50 sub-agent calls by default (Pydantic, 2026f) |
The repair policy is left to the caller. A failure stops the run unless the caller sends a new task; the library has no blocked cone and no drain or stop policy, and the failure report names what failed only if the step's exception says so.
Task decomposition is applied in long-horizon domains whose results can be verified, with coding agents and web agents as the two standard cases; the fitting architecture is determined by whether the procedure is known in advance, whether the environment is known before the run, and whether latency or token cost is the binding constraint.
A coding agent changes a code base to complete a task, such as fixing a reported bug; a web agent completes a task by operating a web browser. Both tasks take many steps, and both are built from the parts of this lecture: the decomposition strategy fixes the order of the sub-tasks, and the executor of each sub-task is a short ReAct loop that calls the tools (see Two Levels: Strategy and Executor).
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.
The choice among the architectures of this lecture follows from seven properties of the task, tested in a fixed order: the first question answered yes decides.
| Question | If yes → | Why |
|---|---|---|
| Is the "how" known and repeatable? | a fixed workflow in code | the order of the steps is fixed in code, so the model cannot produce a wrong plan |
| Is the environment unknown until observed? | ReAct, or a fixed graph with agentic nodes (the Hybrid row of Plan Provenance) | a planner that writes the plan before acting would have to enumerate what it cannot yet observe |
| Must the plan be inspectable, approvable or resumable? | a plan object written before the first action — any decomposition-first architecture with one call per step; a plan file when a person approves it | only a planner call before any action leaves the plan as an artifact outside the model's context |
| Are the steps independent, and is latency the constraint? | an explicit DAG with ready-set dispatch (LLMCompiler) | the run takes the length of the critical path instead of the sum of all step durations |
| Are the steps dependent, the environment known, and tokens the constraint? | ReWOO: one plan, variable bindings, no replanning | the planner and the solver are each called once, so the growing context is not sent to the model again at every step |
| Is it unknown in advance which sub-tasks are too hard for the executor? | decomposition on failure (ADaPT): the executor runs first, and only a sub-task it fails on is decomposed | the plan becomes only as fine as the run requires; a sub-task that the executor solves is never decomposed |
| Does the plan go stale during the run? | replanning — coarse (regenerate the remainder) or fine (insert · prune · re-edge) | a plan written before the run cannot use what the run reveals; replanning passes the new observations to the planner |
Design rule. Every boundary created by the decomposition carries a check; the design states what the check reads, the retry cap $\kappa$, and what a failure report that remains after $\kappa$ attempts triggers: replanning, with its cap $\kappa_{\text{plan}}$, or the end of the run. A check that a program can perform belongs inside the step, as $\textbf{Checked-Step}$ or as an assertion per sub-goal (AdaPlanner); the published planning architectures contain no such check, so the designer adds it. A boundary without a check makes the context smaller and allows parallel execution, but it does not raise reliability.
lecture-Decomp-exercises.ipynb — by hand, in the order of the lecture: the reliability of a
chain with and without a check, false positives, formal vs. semantic checks; the design dimensions of
an architecture, a check per step vs. a check per graph; a plan's DAG, its topological orders and
their check, and a trace of the scheduler with a failing step and its repair.
lecture-Decomp-lab.ipynb — Pydantic AI and its graph library pydantic-graph, introduced as tutorials: an agent with a typed output, a graph of steps with a fork and a join, and a run stepped through event by event, in which the caller repairs a failing step with a new task; then ReWOO's planner, worker and solver on a typed plan that a program checks and compiles into a graph of steps, which executes independent steps concurrently, and a loop that replans when a step fails.
Part I — boundaries and checks
Part II — the plan as an object
Part III — scheduling and recovery
Part IV — domains and architecture choice
Peer-reviewed papers
w/o Tool ablation and the generation–discrimination–critique gap.Preprints & other non-peer-reviewed papers
#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.Textbooks & monographs
Blogs, documentation & other web sources
plan.md and kept in sync with the diff. ⚠ Reports no measurements; cited for the workflow only.cleanupPeriodDays.plansDirectory.GraphRun, ErrorMarker, override_next.UsageLimits, a request limit of 50 by default.