How AI Agents WorkCourse map

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:

Five ways to plan, from none to a tree
DesignWhere the plan livesWho updates it
No explicit planNowhere. Only the history of past steps.Nobody
Plan in plain textA message in the conversationThe model, by writing a new plan
Todo listData in your code's stateThe model, through a tool
Planner and executorThe output of a separate planner callThe planner, when a step fails
HierarchyA tree of goals and subgoalsEach 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:

  1. User Fix the failing auth test.
  2. Model run_command("pytest tests/test_auth.py")
  3. Tool 1 failed: test_login_expired_token
  4. Model read_file("auth/session.py")
  5. 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:

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:

Where an explicit plan tends to pay off
Tends to help whenTends to add overhead when
The task is long, and its steps depend on each otherThe task takes a handful of steps
Work must survive compaction, a restart, or a handoffEach step depends on what the last one revealed, as in debugging
A person should approve the approach before anything changesThe plan would only repeat the request
Several workers need one shared list of what is leftKeeping 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:

  1. Model edit_file("tests/test_auth.py", old="== 401", new="== 200")
  2. Tool pytest: 12 passed
  3. Review "All tests pass. The fix looks correct."
  4. 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

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?

  1. Whether the code style is consistent
  2. Whether the run changed the tests that define success
  3. Whether the model sounded confident
Answer

A passing test proves something only if it still tests the bug, so edited tests come first.

Takeaways

Going deeper