Agents From First Principles 04: AI Agent Fails on Multi-Step Tasks? Separate Planning From Execution
A surprising number of agent failures are not really model failures.
The model may be perfectly capable of writing each individual step. The failure happens because the system tries to decide what to do and do it at the same time.
That works for simple tasks:
question
↓
model
↓
answer
It becomes fragile when success depends on several ordered actions:
goal
↓
step 1
↓
step 2
↓
step 3
↓
verification
A useful next step in agent design is therefore to separate two jobs:
planning
↓
execution
This post builds that split from first principles.
If you found this article because your agent skips steps, executes things in the wrong order, repeatedly replans, follows stale plans, or claims success before the task is complete, this is the mechanism to inspect.
The failure pattern
Imagine asking an agent:
Inspect a project, find the failing test, determine the cause, patch the code, rerun the relevant tests, and report what changed.
A one-shot agent may produce something like:
I'll inspect the project, patch the bug, and run the tests.
Then it immediately edits a file.
The problem is not necessarily that the model is weak.
The problem is that the system never represented the dependency structure explicitly.
A better decomposition is:
1. inspect repository
2. identify failing test
3. reproduce failure
4. locate responsible code
5. patch code
6. rerun focused test
7. rerun broader validation
8. summarize result
Now the runtime has something concrete to execute and inspect.
Planning is not reasoning magic
Planning is simply the act of constructing an intermediate representation of intended work.
The simplest possible plan can be a list:
plan = [
"inspect repository",
"run failing test",
"identify root cause",
"patch code",
"rerun test",
]
That alone is useful because it moves information out of an opaque generation and into explicit program state.
The planner does not need to be a special model.
It might be:
def plan(goal, llm):
return llm(f"Break this goal into ordered executable steps:\n{goal}")
The important architectural change is not the prompt.
It is the interface:
goal
↓
planner
↓
plan
↓
executor
Build the smallest planner/executor agent
Start with a structured step.
from dataclasses import dataclass
@dataclass
class PlanStep:
id: int
instruction: str
status: str = "pending"
A plan is then ordinary program state:
plan = [
PlanStep(1, "inspect repository"),
PlanStep(2, "run the failing test"),
PlanStep(3, "identify root cause"),
PlanStep(4, "patch the code"),
PlanStep(5, "rerun the test"),
]
The executor does not need to decide the whole task again.
def execute_step(step, context, model):
prompt = f"""
Goal:
{context['goal']}
Current step:
{step.instruction}
Known observations:
{context['observations']}
Perform only the current step.
"""
return model(prompt)
Then the runtime controls progression:
for step in plan:
result = execute_step(step, context, model)
context["observations"].append(result)
step.status = "done"
Already we have something far easier to debug than one giant prompt.
But a list is not yet a good plan
Real tasks have dependencies.
Suppose the planner returns:
1. patch the bug
2. reproduce the bug
3. locate the failing code
Every step may sound individually reasonable, but the order is wrong.
So plans need validation just like tool calls did in the previous article.
A useful plan step should answer at least:
what action?
what inputs does it require?
what result should it produce?
what depends on it?
A better structure is:
@dataclass
class PlanStep:
id: str
action: str
depends_on: list[str]
expected_output: str
status: str = "pending"
Example:
PlanStep(
id="reproduce",
action="run the failing test and capture the error",
depends_on=[],
expected_output="a reproducible failure and error trace",
)
Then:
PlanStep(
id="diagnose",
action="identify the root cause",
depends_on=["reproduce"],
expected_output="a concrete explanation tied to code",
)
Now dependency errors are mechanically visible.
Validate before executing
Do not assume a plan is valid merely because an LLM produced it.
A minimal validator can catch a surprising amount.
def validate_plan(steps):
ids = {step.id for step in steps}
if len(ids) != len(steps):
return False, "duplicate step ids"
for step in steps:
for dep in step.depends_on:
if dep not in ids:
return False, f"unknown dependency: {dep}"
return True, None
Then add cycle detection.
def has_cycle(steps):
graph = {s.id: s.depends_on for s in steps}
visiting = set()
visited = set()
def visit(node):
if node in visiting:
return True
if node in visited:
return False
visiting.add(node)
for dep in graph[node]:
if visit(dep):
return True
visiting.remove(node)
visited.add(node)
return False
return any(visit(node) for node in graph)
This is a recurring principle throughout agent design:
Move as much correctness as possible out of probabilistic model behaviour and into deterministic runtime checks.
The planner should not execute
A common architecture mistake is letting the planner both describe and perform work.
For example:
Plan the task and perform the first step.
That collapses the separation again.
Prefer:
planner
↓
structured plan
then:
executor
↓
one step
Why?
Because now we can test each component independently.
If execution fails, ask:
Was the plan wrong?
or:
Was the plan correct but the executor failed?
Those are very different engineering problems.
Planner failure vs executor failure
This distinction should become explicit telemetry.
Imagine the goal is successfully decomposed:
inspect → reproduce → diagnose → patch → verify
but the patch is wrong.
That is an execution failure.
Now imagine the plan omits verification entirely:
inspect → patch → report success
That is a planning failure.
Track them separately.
metrics = {
"plan_valid": True,
"steps_total": 5,
"steps_completed": 4,
"execution_failures": 1,
"replans": 0,
"goal_verified": False,
}
Without this distinction, teams often “improve the agent prompt” when the real problem is the executor or tool layer.
Search problem: my AI agent skips steps
If your agent skips steps, inspect the state transition first.
A fragile implementation looks like:
next_step = model(context)
execute(next_step)
The model is free to forget previously intended work.
With an explicit plan:
pending = [s for s in plan if s.status == "pending"]
step = next_ready_step(pending)
The runtime retains responsibility for sequencing.
A simple readiness rule is:
def ready(step, completed):
return all(dep in completed for dep in step.depends_on)
This turns “remember the plan” from an LLM responsibility into a program invariant.
Search problem: my agent executes steps in the wrong order
This usually means dependencies are implicit.
If order matters, represent it.
Do not rely on prose like:
Make sure to test before deploying.
Represent the dependency:
deploy.depends_on = ["test"]
Then the runtime can reject impossible transitions.
if not ready(step, completed):
raise RuntimeError("step dependencies not satisfied")
This is much stronger than asking the model to be more careful.
Search problem: my agent keeps replanning forever
Replanning is useful only when the world invalidates the current plan.
A bad loop is:
plan
↓
execute one step
↓
plan again
↓
execute one step
↓
plan again
That can create enormous token usage and plan instability.
Instead define explicit replan triggers.
For example:
REPLAN_REASONS = {
"dependency_failed",
"new_constraint",
"tool_unavailable",
"unexpected_observation",
"plan_exhausted_but_goal_unmet",
}
Then:
if observation.reason in REPLAN_REASONS:
plan = replan(goal, current_plan, observation)
Do not replan merely because another model call is available.
Preserve completed work during replanning
A second replanning bug is throwing away useful progress.
Suppose we have:
✓ inspect repository
✓ reproduce failure
✓ identify module
✗ patch attempt failed
A naive replanner may regenerate:
1. inspect repository
2. run tests
3. inspect repository again
Instead feed the planner explicit state:
replan_input = {
"goal": goal,
"completed_steps": completed_steps,
"failed_step": failed_step,
"observations": observations,
}
And require:
Preserve completed valid work.
Plan only the remaining path to the goal.
Better still, validate that completed steps are not reintroduced unless there is evidence they need repeating.
Stale plans
Plans are hypotheses about the future.
The environment may change.
A plan created at time t0 might assume:
API endpoint exists
but after an observation:
404 endpoint removed
the plan is stale.
So every plan should be treated as conditional on current state.
plan
+
new observation
↓
still valid?
This does not mean asking the LLM to rethink everything after every step.
Often a cheap deterministic check is enough:
if required_resource_missing:
mark_plan_stale()
Only then invoke replanning.
Executor drift
Another common failure is that the executor ignores the plan and starts doing extra work.
Suppose the current step is:
run the failing unit test
but the model decides to:
rewrite the test suite
That is executor drift.
One defence is to give the executor a narrow contract:
You are executing exactly one plan step.
Do not add, reorder, or invent plan steps.
Return:
- action taken
- observation
- whether the step's expected output was produced
And then validate the action against the step.
The planner decides what should happen.
The executor decides how to carry out the current approved step.
Expected outputs matter
A plan step without a success condition is hard to verify.
Weak:
Investigate the error.
Better:
Investigate the error and produce a root-cause statement that names the failing component and cites the observed error.
Now the runtime can ask:
Did this step actually produce the expected artifact?
This is the beginning of verification-driven execution.
Plan as data, not prose
Prose plans are convenient for humans but harder for runtimes.
Prefer a structure such as:
{
"steps": [
{
"id": "reproduce",
"action": "run focused test",
"depends_on": [],
"expected_output": "captured failing test result"
},
{
"id": "diagnose",
"action": "identify root cause",
"depends_on": ["reproduce"],
"expected_output": "root cause tied to code"
}
]
}
That enables:
- schema validation
- dependency checks
- status updates
- telemetry
- resumption
- partial replanning
- visualization
The plan becomes an inspectable execution graph.
A small planner/executor runtime
Here is a compact implementation.
from dataclasses import dataclass, field
@dataclass
class PlanStep:
id: str
instruction: str
depends_on: list[str] = field(default_factory=list)
expected_output: str = ""
status: str = "pending"
observation: str | None = None
class PlannerExecutorAgent:
def __init__(self, planner, executor, verifier, max_replans=2):
self.planner = planner
self.executor = executor
self.verifier = verifier
self.max_replans = max_replans
def run(self, goal):
plan = self.planner(goal)
self._validate(plan)
completed = set()
replans = 0
while True:
ready = [
step for step in plan
if step.status == "pending"
and all(dep in completed for dep in step.depends_on)
]
if not ready:
break
step = ready[0]
observation = self.executor(goal, step, plan)
step.observation = observation
ok = self.verifier(step, observation)
if ok:
step.status = "done"
completed.add(step.id)
continue
step.status = "failed"
if replans >= self.max_replans:
return {
"success": False,
"reason": "replan_budget_exhausted",
"plan": plan,
}
plan = self.planner(
goal,
previous_plan=plan,
failure=observation,
)
self._validate(plan)
replans += 1
success = self._goal_complete(plan)
return {
"success": success,
"reason": "goal_complete" if success else "plan_exhausted",
"plan": plan,
"replans": replans,
}
def _validate(self, plan):
ids = {step.id for step in plan}
if len(ids) != len(plan):
raise ValueError("duplicate plan step id")
for step in plan:
missing = set(step.depends_on) - ids
if missing:
raise ValueError(f"unknown dependencies: {missing}")
def _goal_complete(self, plan):
return all(step.status == "done" for step in plan)
This is intentionally simple.
The point is the architecture:
planner
↓
validated plan
↓
ready-step selection
↓
executor
↓
observation
↓
step verification
↓
continue / replan / stop
Planning does not automatically improve performance
A planner adds cost.
At minimum:
extra model call
extra tokens
extra latency
new failure surface
So the correct comparison is not:
agent with planner sounds more advanced
It is:
one-shot or reactive agent
vs
planner/executor agent
on the same task distribution.
Measure:
verified success rate
steps completed correctly
unnecessary steps
replan count
model calls
latency
cost
Planning should earn its place.
Tasks where planning is likely to help
Planning becomes useful when tasks contain dependencies.
Examples:
code change
research synthesis
data migration
multi-tool workflows
deployment procedures
investigation / diagnosis
long-form artifact creation
A useful heuristic is:
If doing step B correctly depends on information produced by step A, explicit planning may help.
Tasks where planning may hurt
Do not plan everything.
For a simple transformation:
rewrite this sentence more clearly
adding:
planner → executor → verifier
is probably wasteful.
Likewise:
classify sentiment
extract one field
convert units
summarize one short paragraph
Planning can increase latency without improving correctness.
Plans can over-constrain the executor
There is another trade-off.
A detailed plan may lock the system into a weak early assumption.
Suppose the planner decides:
Step 3: edit parser.py
but execution reveals the bug is actually in:
tokenizer.py
A rigid executor may continue following the wrong plan.
Therefore plans should encode intent and dependencies without pretending the future is known exactly.
Better:
identify and patch the component responsible for the reproduced failure
than:
edit parser.py line 217
unless evidence already supports that location.
Hierarchical planning
For very large tasks, one flat list becomes unwieldy.
You can introduce hierarchy:
goal
├─ investigate
│ ├─ reproduce
│ └─ diagnose
├─ implement
│ └─ patch
└─ validate
├─ focused tests
└─ broader tests
But do not start here.
A flat plan is easier to inspect and often sufficient.
Hierarchy is another mechanism that should be justified by task complexity.
Dynamic vs static planning
There are two useful extremes.
Static plan
Generate once:
plan → execute all steps
Advantages:
- cheap
- stable
- predictable
Disadvantages:
- becomes stale
- weak under unexpected observations
Dynamic replanning
plan
↓
execute
↓
observe
↓
replan when necessary
Advantages:
- adapts
- handles failures
Disadvantages:
- more calls
- instability
- potential loops
The useful middle ground is usually:
Plan once. Replan only on explicit evidence that the current plan is invalid.
Plan versioning
Once replanning exists, version plans.
plan_version = 1
After a replan:
plan_version += 1
Log:
plan_v1
step executed
observation
replan reason
plan_v2
This makes it possible to answer:
Why did the agent change direction?
Without plan history, adaptive systems become very difficult to debug.
Planner telemetry
At minimum record:
{
"plan_version": 2,
"steps": 6,
"completed": 4,
"failed": 1,
"replans": 1,
"replan_reason": "tool_unavailable",
"goal_verified": False,
}
Useful aggregate metrics include:
plan validity rate
average plan length
step completion rate
replan frequency
unnecessary-step rate
executor drift rate
verified task success
How to test whether the planner is actually useful
Build an experiment matrix.
A: one-shot model
B: reactive step-by-step agent, no explicit plan
C: static planner + executor
D: planner + executor + bounded replanning
Use the same tasks.
Track:
verified success
model calls
steps
latency
cost
recovery after injected failure
The interesting result may be:
simple tasks → A wins
medium tasks → C wins
uncertain environments → D wins
That is more useful than claiming one architecture is universally best.
Inject failures deliberately
A good planner/executor test suite should include failures such as:
missing file
tool timeout
unexpected API response
invalid intermediate result
resource disappears
step returns incomplete evidence
Then ask:
Does the agent recover?
Does it replan only when necessary?
Does it preserve completed work?
Does it stop when recovery is impossible?
Agent reliability is often visible only when the happy path breaks.
Debugging checklist: AI agent fails on multi-step tasks
The agent skips steps
Check:
Is the plan explicit state?
Does the runtime select pending steps?
Are completed steps tracked?
The agent performs steps in the wrong order
Check:
Are dependencies represented?
Does the runtime enforce them?
The agent keeps replanning
Check:
Are replan triggers explicit?
Is there a replan budget?
Are repeated identical plans detected?
The agent repeats completed work
Check:
Is completed state passed to replanning?
Does the new plan preserve valid progress?
The executor ignores the plan
Check:
Is it asked to execute exactly one step?
Are actions validated against current-step scope?
The plan looks good but results are bad
Check the executor before rewriting the planner.
The agent reports success too early
Check:
Does the final goal have an external completion condition?
Is plan exhaustion incorrectly treated as success?
Plan completion is not goal completion
This distinction is critical.
Suppose every plan step ran successfully:
✓ inspect
✓ patch
✓ test
but the test still fails.
The plan completed.
The goal did not.
Therefore:
all steps done
must not automatically imply:
goal achieved
Have a goal-level verifier.
success = verify_goal(final_state)
This idea will become central later in the series.
The planning hierarchy so far
We have now accumulated several agent mechanisms.
Step 00
observe → decide → act
Step 01
structured action → validate → execute
Step 02
generate N → score → select
Step 03
draft → critique → revise → gate
Step 04
goal → plan → execute → observe → replan when needed
Notice what is happening.
The model itself did not need to change.
We are changing the computation around the model.
That is one of the deepest ideas in agent engineering.
Do you actually need a planner?
Use this decision rule:
Does the task contain dependent steps?
│
├─ no → probably do not add planning
│
└─ yes
↓
Can a fixed deterministic workflow encode them?
│
├─ yes → prefer the workflow
│
└─ no
↓
Does the route depend on observations?
│
├─ no → static plan may be enough
│
└─ yes → planner + bounded replanning
The planner is not the goal.
Reliable task completion is the goal.
Final rule
A useful planner/executor architecture obeys five rules:
1. represent the plan explicitly
2. validate the plan before execution
3. execute one approved step at a time
4. replan only when observations invalidate the current path
5. verify the goal independently of plan completion
That gives us a system we can inspect rather than a prompt we can only hope behaves correctly.
Next: what happens when actions fail?
Planning gives the agent a route.
But real environments do not cooperate.
Tools time out.
Files are missing.
APIs return unexpected data.
A command succeeds but produces the wrong result.
So the next post moves from planning into the runtime loop itself:
Agents From First Principles 05: AI Agent Gets Stuck in a Loop? Add State, Feedback and Stopping Conditions.