Chapter 6 · Capabilities built on the loop
Planning and reflection
Plans and reviews often help on long tasks. But they cost extra calls and can go wrong, so measure them on your own work.
What "the agent plans" means
Ask Patch to fix the failing auth test, and it may start with a plan. The plan says to reproduce the failure, find the cause, fix it, and run the tests. When people say "the agent plans", they mean a written list like this, which later steps can use.
A useful plan is updated as steps finish and facts change. Many models also reason before they reply. Some providers hide that reasoning, and others return it so your code can store it. Either way, it is not a list that later steps can read and update item by item.
There are five common designs, and they differ in where the plan lives:
| Design | Where the plan lives | Who updates it |
|---|---|---|
| No explicit plan | Nowhere. Only the history of past steps. | Nobody |
| Plan in plain text | A message in the conversation | The model, by writing a new plan |
| Todo list | Data in your code's state | The model, through a tool |
| Planner and executor | The output of a separate planner call | The planner, when a step fails |
| Hierarchy | A tree of goals and subgoals | Each level, for its own branch |
No explicit planning
In the simplest design, nobody keeps a plan. On each call, the model reads the request and the history and picks the next action, as in the loop from chapter 1:
- User Fix the failing auth test.
- Model
run_command("pytest tests/test_auth.py") - Tool 1 failed:
test_login_expired_token - Model
read_file("auth/session.py") - Model
edit_file("auth/session.py", ...), then runs the tests again
Nothing records an intention beyond the next step, because the history holds only the past. This is often the best choice for short tasks. It also suits exploratory work such as debugging, where each step depends on what the last one revealed.
On long tasks it tends to struggle. Subgoals get lost when old history is trimmed, and the model can wander or stop with half the work undone.
A plan in plain text
One step up, the model writes its plan as text in a reply before it acts:
Plan:
1. Inspect config
2. Find authentication logic
3. Reproduce bug
4. Patch
5. Test
This costs almost nothing, and it often helps. The model commits to an order, and later calls see the plan in the history. But the plan is only text, so nothing tracks which steps are done. As the run grows, compaction (shortening old history to save room) can squeeze the plan into a summary. If the model changes course, a second plan joins the first in the history.
A todo list in your code's state
Here your code keeps the plan as data, and the model changes it through a tool. This list starts from a plan that assumes the tests can run as they are:
# step 1: the model calls todo_write with its first plan
[{"task": "Reproduce the failing test", "status": "in_progress"},
{"task": "Find and fix the cause", "status": "pending"},
{"task": "Run the auth tests", "status": "pending"}]
# step 2: pytest cannot reach PostgreSQL, so the assumption was wrong
[{"task": "Start the test database", "status": "in_progress"}, # new
{"task": "Reproduce the failing test", "status": "pending"},
{"task": "Find and fix the cause", "status": "pending"},
{"task": "Run the auth tests", "status": "pending"}]
# step 7: database up, failure reproduced, grace period removed
[{"task": "Start the test database", "status": "completed"},
{"task": "Reproduce the failing test", "status": "completed"},
{"task": "Find and fix the cause", "status": "completed"},
{"task": "Run the auth tests", "status": "in_progress"}]
At step 2 the tests could not run, because no database was running. So the model added an item to the plan, instead of only changing its next action. Because the plan is data, a person or a check can see that change.
How the plan reaches the model
Keeping the plan in state does not put it in front of the model. You also choose how it gets into the context:
- History only. The model sees the plan through its own earlier tool calls. This is simple, but old calls can be trimmed away. Deep Agents (0.7.28) works this way.
- Every call. Your code adds the full list to each context, right before the newest message. The plan never scrolls away, but it costs tokens on every call.
- A short view. Your code adds only the current item, the next two, and a count. It shows the full list after compaction.
def render_plan(todos):
done = [t for t in todos if t["status"] == "completed"]
now = [t for t in todos if t["status"] == "in_progress"]
upcoming = [t for t in todos if t["status"] == "pending"][:2]
lines = [f"Plan: {len(done)} of {len(todos)} done."]
lines += ["NOW: " + t["task"] for t in now]
lines += ["NEXT: " + t["task"] for t in upcoming]
return "\n".join(lines)
Start with the cheapest option that keeps the agent on track, and measure.
Plan approval
A written plan also gives a person something to approve before the first edit (chapter 11). But approving a plan does not approve every action in it. "Update the schema" says nothing about the exact migration, so keep a separate gate on dangerous steps.
Enforcement matters too. Hermes (v0.22.2) has a /plan mode that only asks the model not to edit files. An example plan mode for Pi (1.3.0) turns off the edit and write tools and lets bash run only read-only commands. It needs both, because a shell that can run any command could still change files. Claude Code's plan mode is enforced by its permission system. The model may read files and run shell commands to explore. But edits stay blocked until you approve the plan, which the model presents with an ExitPlanMode tool. Shell commands outside a read-only list either ask you first or go through a safety check by a classifier.
Planner and executor
Here two parts split the work. A planner call turns the goal into steps. An executor carries out one step at a time, often as its own loop with tools. When a step fails, the planner revises the rest of the plan:
def plan_and_execute(goal, replans_left=3):
plan = planner(goal) # one model call: a list of steps
done = []
while plan:
step = plan.pop(0)
result = executor(step, notes=done) # a loop with tools, or one call
done.append((step, result))
if result.failed:
if replans_left == 0:
return "stopped: too many replans"
replans_left -= 1
plan = planner(goal, done=done, failed=step) # revise what is left
return summarize(done)
With this split, the planner can be a stronger model and the executor a cheaper one, and the executor's context stays small. The cost is that information must cross the boundary. The executor may discover something that changes the whole plan but report only "step failed", so the planner revises from a summary. Each replan also adds calls, and a weak planner can loop on the same failing step.
A plan fixed in advance can also limit prompt injection, which means instructions hidden in text the agent reads. If the plan comes only from the trusted request, a web page the executor reads later cannot add steps. It can still change what a step does, though. Security researchers call this the Plan-Then-Execute pattern, and chapter 14 compares it with other designs.
Hierarchical planning
Goal: ship password reset
├── API endpoint one executor per branch
│ ├── reset token: single-use, 30 minutes
│ └── POST /password-reset, rate limited
├── Email template
├── Front-end form (TypeScript)
└── Tests
Large goals split into subgoals and tasks, and hierarchical planning keeps that tree written down. Each branch can get its own executor, often a subagent, which is a second agent loop with a fresh context. The results flow back up, and the parent checks them against the goal.
The tree helps when branches are independent and large, but it hurts when they are coupled. For example, the API and the form must agree on one request format, and two branches that decide it separately can disagree. Every level also adds calls and chances to lose detail (chapter 7).
When planning helps, and when it adds overhead
A one-line typo fix gains nothing from a plan. A change that touches the API, the email template, the form, and the tests probably does:
| Tends to help when | Tends to add overhead when |
|---|---|
| The task is long, and its steps depend on each other | The task takes a handful of steps |
| Work must survive compaction, a restart, or a handoff | Each step depends on what the last one revealed, as in debugging |
| A person should approve the approach before anything changes | The plan would only repeat the request |
| Several workers need one shared list of what is left | Keeping the plan current costs more calls than the work |
Each plan update is a tool call. A detailed plan written before the agent looks at anything is mostly guesses, and the model may keep following it anyway. In one Anthropic test of building a retro game maker, a single agent took 20 minutes and $9, and its game was broken. A harness with separate planner, builder, and evaluator agents took 6 hours and $200, and its game was playable.
So test a plan like any other feature, by comparing runs with and without it on the same tasks (chapters 16 and 18).
Reflection
Patch fixes the auth bug, the tests pass, and then a second model call reviews the change before Patch reports success. That review is reflection: one more model call that reads earlier work, such as a diff or the record of a run. It returns a critique, a revision, or a verdict.
review = model([
system("You review a coding agent's change. List concrete problems only."),
user(f"Task: {task}\nDiff:\n{diff}\nTest output:\n{test_output}"),
])
if review.problems:
messages.append(user("The review found problems:\n" + review.text))
# the loop continues, and the agent revises with the review in its context
Self-critique without evidence
The weak form asks the same model, with the same information, "Is this right? Improve it." No new information enters, so the reviewer shares the blind spots that produced the work. It can approve a wrong answer with confidence. It can also change a right answer into a wrong one, because the question suggests that something is wrong.
Revision from evidence
The strong form adds a signal from outside the model, such as test results, a type checker, or the requirements. "The test still fails with assert 200 == 401" tells the model far more than "please double-check". Evidence can mislead too, though:
- Model
edit_file("tests/test_auth.py", old="== 401", new="== 200") - Tool
pytest: 12 passed - Review "All tests pass. The fix looks correct."
- Your code A test file changed in the same run as the fix, so it asks a person.
The tests pass because the edit changed the test, not the code. So a good review asks what the evidence proves. Did any test change, and does a test still check that an expired token gets a 401? Better still, put a rule in your code: a fix may not edit the test that defines success unless a person allows it.
Costs and limits
- Cost: every review round is at least one more model call.
- Shared mistakes: a model reviewing its own work tends to repeat its errors. Anthropic engineers report that agents "reliably skew positive when grading their own work", and that a separate, skeptical evaluator is easier to tune.
- No guaranteed gain: a revision can be worse than the original, so keep the original, compare, and cap the number of rounds.
Running example: Patch gets a plan and a review
Patch version 6 gets a todo_write tool that replaces the list in its state. Your code shows the model a short view of the list on every call. A plan that touches more than five files or includes a migration needs a person's approval before the first edit. Before Patch reports success, a review call checks the diff against the test results.
+ tools += [todo_write] # replaces state["todos"]
+ context.add_before_last(render_plan(state["todos"])) # short view on each call
+ if needs_plan_approval(state["todos"]): wait_for_human("approve the plan")
+ if model_says_done and tests_still_pass(state):
+ if tests_edited(state): ask_human("a test changed in this run")
+ elif review(diff(state), state["last_test"]).ok: return "done"
What goes wrong
Following a stale plan
- Why it happens
- The plan was written before the agent knew much. New evidence contradicts it, but the model keeps ticking items off.
- How to spot it
- Look for items marked done without the evidence they promised.
- What to do
- Tie each item to evidence, replan when an assumption fails, and keep early plans short.
A review that approves everything
- Why it happens
- The reviewer is the same model with the same context, so it shares the author's blind spots.
- How to spot it
- Reviews approve almost every change, even ones that fail later.
- What to do
- Give the reviewer outside evidence, a different prompt or model, and checks in code.
A gamed success check
- Why it happens
- The agent changes the test, or the check, instead of fixing the code.
- How to spot it
- Look for diffs that change tests in the same run as the fix.
- What to do
- Protect the success criteria. Test edits need approval, and your code checks against the original tests.
You decide, AI builds
Decide yourself
- Whether a kind of task needs a plan at all, based on measured results.
- How the plan reaches the model, and which plans need a person's approval.
- What evidence a review must see, and which success criteria the agent may never change.
Delegate to AI
- The todo tool, its schema, and the code that updates state.
- Rendering the plan into the context, and the approval screen.
- The review call, and counting its outcomes for evaluation.
Check yourself
What is planning, mechanically? (Q10)
Answer
Keeping a list of intended steps where later steps can read and change it. The list can be text in the conversation, a todo list in your code's state, or a planner's output that an executor follows.
What is reflection, mechanically? (Q11)
Answer
Another model call that reads earlier work, such as a draft or a diff, and returns a critique or a verdict. It helps most with outside evidence, costs extra calls, and can repeat the original mistakes.
A review call approves Patch's fix because the tests pass. What should it check first?
Answer
A passing test proves something only if it still tests the bug, so edited tests come first.
Takeaways
- Keep the plan where your code can see and update it. Decide separately how much of it the model sees on each call.
- Planning tends to help on long, dependent, or approved work, and to waste calls on short or exploratory work.
- A review helps when it brings in outside evidence, such as test results, and even that evidence needs checking.
Going deeper
- Reflexion: Language Agents with Verbal Reinforcement Learning (2023). Lessons from feedback, stored and used on the next try.
- Harness design for long-running application development (Anthropic, 2026). Planner, builder, and evaluator agents, and their cost.
- Deep Agents pull request #4929. The three-model test behind making the todo tool optional.
- Mario Zechner, "What I learned building an opinionated and minimal coding agent". The case for plans in files.