Agents From First Principles 04: AI Agent Fails on Multi-Step Tasks? Separate Planning From Execution

Page content

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.