Why architecture matters here
Agents fail on the plan, not on the individual step. A step-by-step call to an LLM is straightforward; recognizing that Step 3 needs a different tool because Step 2 returned unexpected data is where good planners earn their keep.
The architecture matters because the loop must be observable. Without a state store, the agent forgets what it decided. Without reflection, it cannot correct. Without replan, it charges ahead when off-course. Without budgets, it never stops.
Build the loop and the agent gets much smaller — the LLM contributes one piece of judgment at a time within a reliable structure.
The architecture: every piece explained
The top strip is the forward path. Goal captures user intent explicitly with acceptance criteria. Planner LLM produces a high-level plan (natural language + structured steps). Decomposer breaks steps into concrete actions with dependencies. Executor invokes tools or delegates to subagents, one step at a time.
The middle row is the control loop. State store holds task board (planned/in-progress/done), facts learned so far, and current beliefs. Reflection evaluates recent progress against the plan — did the step succeed, did we get new information? Replan revises the plan when reflection reveals a gap. Completion check asks whether the goal is satisfied and returns to the user with results.
The lower rows are hygiene. Memory + summarization compacts context so long agents stay within token limits. Guardrails + policy enforce that every action passes safety checks — the governance plane from a separate article. Observability logs the plan, every step, the reflection outputs, and cost.
End-to-end flow
End-to-end: a user asks an agent to "triage this bug report and propose a fix." Planner LLM outputs plan: 1) read the report, 2) find the relevant code, 3) reproduce, 4) diagnose, 5) draft fix. Decomposer maps to concrete tool calls. Executor runs step 1 (read repo); state store records key facts. Step 2 grep — one relevant file. Step 3 reproduce — test fails as expected. Step 4 diagnose — LLM analyzes and identifies a null check missing. Reflection: yes, this matches the report. Step 5 draft fix — LLM outputs a patch. Completion check confirms all steps done and returns the patch to the user. Observability shows total 5 tool calls, 12k tokens, $0.14 cost.
What the planner consumes: the goal contract
A planner is a function, and it fails at the input boundary far more often than it fails at reasoning. Four things belong in the call.
The goal, restated with acceptance criteria. Not the raw user turn - a normalised goal plus the explicit conditions that make it satisfied. Forcing the planner to emit acceptance criteria as a first-class field before any steps is the highest-leverage change available: it gives the completion check something to test against instead of letting "done" mean "the model stopped".
The tool catalog, by exact name and schema, pinned to a version. A planner shown prose capability descriptions rather than the literal names and schemas the executor accepts will confidently emit send_slack_message when the runtime only exposes notify_channel. Pin the catalog version into the plan header so a mid-run change is detectable rather than mysterious.
Constraints. Budget, deadline, which side effects are allowed without approval. A planner that does not know the budget cannot trade breadth for depth, and will emit a forty-step plan for a job priced at twelve.
Known world state. Facts already established - files read, IDs resolved, prior results - so the plan does not re-derive what the run knows. The anti-pattern is passing the whole transcript as "context": the planner then plans around the dialogue rather than the goal, and plan structure becomes unstable turn to turn.
Plan shape: flat list, graph, or hierarchy
The representation is not cosmetic. It sets an upper bound on how surgically the plan can be repaired later, because you can only re-expand a subtree if the plan has subtrees.
| Shape | What it buys | Repair granularity |
|---|---|---|
| Flat ordered list | Trivial to generate, validate, and show to a user | Two moves only: patch a step, or discard the plan |
| DAG with declared dependencies | Exposes independence so the scheduler can fan out - mechanics in task decomposition | Replan affected descendants; other branches survive |
| Hierarchical (goal, subgoals, steps) | Defers detail: expand a subgoal only when its turn arrives | Re-expand one subgoal, leaving the rest untouched |
Whether to commit to a plan at all reduces to one criterion: commit up front only when the step set is knowable before the first observation. Goals whose next action genuinely depends on what the last one returned - exploratory research, debugging, anything driven by search results - belong in an interleaved reasoning-and-acting loop, covered in ReAct, with framework-level mode selection in ADK planning and the ADK-Java planner. The hybrid that survives production is a coarse plan of three to seven subgoals, each executed by an interleaved loop, revised only at subgoal boundaries.
One trap: do not put control flow in the plan unless the executor implements it. A step reading "if step 3 returns nothing, do X instead" is a silent no-op against an executor that walks a list, and no test catches it because the plan reads correctly to a human.
Validating the plan before a single tool fires
Every plan should pass a deterministic static check before execution starts. It costs microseconds and converts the most expensive failure mode - discovering at step 9 that step 10 was never runnable - into an instant, repairable error.
def validate(plan, catalog, budget):
errs, produced = [], set()
for i, s in enumerate(plan["steps"]):
tool = catalog.get(s["tool"])
if tool is None: # hallucinated tool
errs.append("step %d: unknown tool" % i); continue
errs += typecheck(s["args"], tool.schema) # args vs JSON schema
for ref in refs_in(s["args"]): # ${step_2.result}
if ref not in produced: # forward / dangling ref
errs.append("step %d: reads undefined %s" % (i, ref))
if tool.side_effecting and not s.get("approval_gate"):
errs.append("step %d: unapproved side effect" % i)
produced.add("step_%d.result" % i)
if has_cycle(plan): errs.append("dependency cycle")
if est_cost(plan, catalog) > budget: errs.append("over budget")
if not covers(plan["steps"][-1], plan["acceptance_criteria"]):
errs.append("terminal step produces no acceptance artifact")
return errsIn rough order of how often they fire: unknown tool name, arguments that fail to typecheck once placeholders are substituted, a step reading an output no earlier step produces, a dependency cycle, a side-effecting step with no approval gate (where the planner meets guardrails), cost over budget, and a terminal step producing nothing that matches the acceptance criteria.
On failure, return the error list to the planner as a repair prompt rather than to the user - one round fixes most schema and reference errors, because the model is told exactly what is wrong in its own vocabulary. Cap repairs at two or three: a planner that cannot produce a valid plan by the third attempt rarely has a reasoning problem, it has a catalog problem, and no re-prompting conjures a tool that does not exist.
Replanning: triggers, staleness, and loop guards
Replanning goes wrong when it is treated as an exception handler. It is a scheduled evaluation with named triggers.
Triggers worth wiring explicitly
A step fails terminally after its retry policy is exhausted. A precondition evaluates false at dispatch. A tool result contradicts a fact the plan assumed. Projected cost or wall-clock for the remaining steps breaches the budget. The catalog version changed mid-run. A verifier rejects a step result. Each is a different signal deserving a different response - collapsing them into "something broke, replan everything" is what produces runaway cost.
Detecting that a plan has gone stale
Staleness is not failure. A plan is stale when an assumption it recorded is no longer true, even though every step so far succeeded. Make assumptions explicit at plan time: each step carries the fact keys it reads and the preconditions it expects. Before dispatch, the executor re-evaluates those preconditions and compares a hash of the referenced facts against their values when the plan was built. A mismatch means the remaining suffix should be re-derived. The check is nearly free and catches the quiet failure where an agent flawlessly executes a plan for last Tuesday.
Prefer the narrowest edit, then stop
Order responses by blast radius: substitute one step's arguments, re-expand a single subgoal, replan only the unexecuted suffix, and only as a last resort replan from the goal. Preserving completed work needs durable step results, the concern of agent checkpointing - a planner that discards finished results on every replan is a cost bug wearing a correctness costume.
Then guard the loop. The classic runaway: the plan calls tool X, X is unavailable, the planner replans, the new plan calls X again - forever, because nothing ever wrote "X is unavailable" into the facts the planner reads. The primary fix is that failures must feed back as facts, not merely as retry counters. Around that, enforce a hard replan budget per run (three to five is typical) and require each new plan to differ structurally from its predecessor: an identical multiset of step signatures is oscillation, not adaptation. A monotonic-progress rule helps - satisfied acceptance criteria must never decrease across a replan. When the guards trip, hand off rather than iterate; see human-in-the-loop.
What the planner itself costs
Planning is one model call, but usually the largest in the run: full tool catalog, goal, constraints, and accumulated facts, typically with extended reasoning on. That is commonly several thousand input tokens and multiple seconds before any user-visible work happens. Amortised over a twenty-step run it is noise; spent on a two-step run it is most of the bill and all of the perceived latency.
So gate it. A cheap classifier or router decides whether a request warrants an explicit plan; an estimated one or two steps should skip planning entirely. Cache plan skeletons keyed by normalised goal plus catalog version. Timebox the planner so a slow or invalid plan degrades into the interleaved loop instead of stalling the request. And budget replans against the same envelope as the initial plan - three replans over a large catalog can cost more than the execution they were meant to rescue. Wider accounting is in agent cost optimization.
Measuring plan quality
Evaluating only outcomes hides a bad planner that aggressive replanning keeps rescuing at triple the cost. Score the plan as its own artifact, independently of execution.
The metrics that discriminate: validity rate, the share of plans passing the static validator first try; acceptance coverage, whether some step produces each stated criterion; step efficiency, emitted steps over a human reference plan, where much above 1.5 signals over-decomposition and much below 0.7 signals steps too coarse to execute; replans per successful run; unreachable-step rate, steps planned but never executed, which measures speculative padding; and cost per successful goal.
Keep a golden set of thirty to fifty goals with human-written reference plans and diff structurally - tools invoked and dependency shape, not the wording of step descriptions. Plans are short, so this evaluation is cheap enough to run on every change to the planner prompt or the catalog, which is where regressions enter. Output-level checks stay separate; see output verification.
Failure modes you will actually hit
Over-decomposition. The planner emits "open the file", "read the file", "close the file" as three steps, each a model call and a state write. Cap depth and fan-out, and require that a step be satisfiable by one tool call.
Plans that assume tools which do not exist. The most common single defect, preventable by pinning the catalog and running the validator. Watch for the subtler variant: a real tool name with an invented parameter.
The plan-shaped answer. A plan that reads beautifully and is unexecutable line by line - vague steps, missing inputs, implied context. Only the static validator catches this; human review does not, because prose plausibility is what the model optimises.
Premature completion. The last step is "summarise the findings", the summary appears, and the run reports success. A completion check that reads the plan rather than the acceptance criteria will always agree with the plan.
When not to build a planner
An explicit planner is negative value when the median run is under three tool calls, or when the tool set is small enough that ordering is obvious. It is the wrong abstraction when the workflow shape is known and fixed - that is a state machine, with guaranteed transitions and none of the variance of asking a model to re-derive the same sequence every run.
The planner earns its slot when a wrong first move is expensive, when independent branches exist and parallelism is worth exposing, when the plan is itself a deliverable an approver should see before execution, or when the work must be priced before it is performed. Those four conditions, not the presence of multiple tool calls, are the test.
Treat the plan as a durable artifact with a lifecycle - validated against a pinned tool catalog before execution, carrying explicit acceptance criteria and per-step preconditions, repaired at the narrowest granularity its representation allows, and bounded by a replan budget with failures fed back as facts. A planner scored only on final outcomes will look fine while replanning quietly pays for its mistakes.