Planner-Executor-Replanner: Let Plans Change
The travel assistant makes a plan: West Lake in the morning, Tea Museum in the afternoon, Hefang Street at night.
Then weather changes, or the museum is closed. The old plan is stale. Plan & Solve keeps running. A free-form agent loop may drift too much. Planner-Executor-Replanner sits between them: plan, execute one step, then decide whether to continue, revise, or finish.
One Sentence
Planner-Executor-Replanner turns plan-once execution into plan → execute → check/replan, so long tasks can correct course.
What Breaks Without It
| Problem | What it looks like | Risk |
|---|---|---|
| One plan runs to the end | Organized | Stale plan keeps running |
| Every step is free-form | Flexible | Goal drift and cost growth |
| No replan point | Simple | Failures propagate |
What This Pattern Changes
| Who | Owns |
|---|---|
| Planner | Creates initial plan |
| Executor | Executes current step |
| Replanner | Decides continue, replan, or final |
| Python | Stores plan, limits steps, traces |
Walk Through One Trace
| Round | Current step | Execution result | Replanner decision |
|---|---|---|---|
| 1 | Step A | did A |
continue |
| 2 | Step B | did B |
final: All done |
In travel, the replanner may replace outdoor afternoon stops after a rain observation.
Flow
flowchart TD
T["Task"] --> P["Planner creates plan"]
P --> E["Executor runs current step"]
E --> R["Replanner checks result"]
R -->|continue| E
R -->|replan| P2["Update remaining plan"]
P2 --> E
R -->|final| O["Final answer"]
Code Walk
The example has three model roles:
planner = MockLLM(['{"plan":["Step A","Step B"]}'])
executor = MockLLM(["did A", "did B"])
replanner = MockLLM(
[
'{"action":"continue","plan":[],"answer":""}',
'{"action":"final","plan":[],"answer":"All done"}',
]
)
Full example:
from __future__ import annotations
from pathlib import Path
from agent_patterns_lab.patterns.planner_executor_replanner import planner_executor_replanner
from agent_patterns_lab.runtime import MockLLM, RunLimits, Tracer
def main() -> None:
tracer = Tracer()
planner = MockLLM(['{"plan":["Step A","Step B"]}'])
executor = MockLLM(["did A", "did B"])
replanner = MockLLM(
[
'{"action":"continue","plan":[],"answer":""}',
'{"action":"final","plan":[],"answer":"All done"}',
]
)
result = planner_executor_replanner(
planner,
executor,
replanner,
task="Do A then B.",
limits=RunLimits(max_steps=5),
tracer=tracer,
)
print(result.answer)
trace_path = tracer.export_jsonl(Path(".traces") / "51_planner_executor_replanner.jsonl")
print(f"[trace] {trace_path}")
if __name__ == "__main__":
main()
Run:
UV_CACHE_DIR=.uv_cache PYTHONPATH=src uv run --no-sync python examples/51_planner_executor_replanner.py
Nearby Patterns
| Pattern | Who decides next | Use when |
|---|---|---|
| Plan & Solve | Execute plan in order | Plan is unlikely to change |
| PER | Replanner revises mid-run | Execution may invalidate plan |
| ReAct | Model chooses each action | No explicit long plan needed |
| LLM Compiler | Plan becomes dependency graph | Tasks can be parallelized |
When To Use It
- The task is long enough for plans to become stale.
- Each step produces observations.
- You want a plan plus controlled revision.
continue/replan/finalactions can be structured.
When Not To Use It
- The task is short.
- Steps are fixed; use workflow.
- There are no observations to replan from.
- The replanner has no criteria.
Costs And Common Failures
| Failure | Symptom | Fix |
|---|---|---|
| Constant replanning | Every step discards plan | Limit replan count |
| Plan drift | User goal disappears | Keep original goal and constraints |
| Vague replanner | No reason for changes | Structured action + reason |
| Long loop | Keeps continuing | Add max_steps |
What To Read Next
PER fits tasks where plans help but may go stale.
If the task can become a dependency graph, read LLM Compiler. If you need to search over plans, read LATS.