TAI BUI
Back to curriculum
CapstonePython

Plan-Execute Control Flow

A plan that cannot survive a failure is a script. A script that can replan is an agent. Build the replanner first.

TIME: ~90 minutesPREREQUISITES: Phase 13 lessons 01-07, Phase 14 lesson 01

Learning Objectives

  • Represent a plan as an ordered list of typed steps so the executor can reason about progress and outcome.
  • Execute steps sequentially with a controlled failure handoff back to the planner.
  • Replan from the current cursor with the prior error in the context so the next plan is informed.
  • Emit a plan diff on each revision so a downstream tracer or UI can show why the plan changed.
  • Enforce two budgets: a hard step ceiling and a hard replan ceiling.

Plan and execute, not chain-of-thought

A plan-and-execute agent emits a structured plan first, then runs each step deterministically through a dispatcher. The plan is data the harness can inspect, and execution is the harness walking that data. A chain-of-thought agent works differently: it emits tokens and lets the loop guess where the tool call ends.

That gives you two pieces. A planner produces the plan and an executor runs it. The interesting work starts when the executor hits a failure, because there are three things it can do:

text
1. Abort         (return failed, surface the error)
2. Skip          (mark step failed, continue with the rest)
3. Replan        (hand the error to the planner, get a new plan from the cursor)

Replan is the one that turns a script into an agent.

Check: Plan and execute, not chain-of-thought0/4
1. What is a plan-and-execute agent?Define
2. A step fails mid-plan. What separates replan from skip as a way of handling it?Explain
3. Why does the lesson say replan is what turns a script into an agent?Explain
4. Step 3 consumes step 2's result. Step 2 fails and the handler marks it failed and carries on. What goes wrong?Analyze

The Step shape

A step is a typed record. The executor fills in result or error as it runs.

text
Step
  id              : int           (monotonic within a plan revision)
  tool_name       : str
  args            : dict
  expected_outcome: str           (planner's stated success condition)
  result          : Any | None
  error           : str | None

expected_outcome is a short sentence the planner emits alongside the step, stating what success looks like. The executor does not enforce it. It has two readers: the replanner, which uses it when revising the plan, and the event stream, which emits it so a tracer can show "this step was supposed to do X."

Check: The Step shape0/3
1. What is a Step in this lesson's plan?Define
2. Why is expected_outcome part of the Step shape when the executor never checks it?Explain
3. A step's expected_outcome says 'rendered output', but the tool returns an empty string and the executor marks the step ok. What assumption was wrong?Analyze

The planner shape

The planner is a pure function from the goal, the execution history, and the last error to a plan.

python
def planner(goal: str, history: list[Step], last_error: str | None) -> list[Step]:
    ...

goal is the user goal. history is the steps already executed, with results and errors filled in. last_error is None on the first call and the most recent failure message on every call after that. The planner returns the next plan starting from the cursor.

It knows nothing about the executor, retries, or timeouts. It produces a plan and stops there.

Check: The planner shape0/3
1. What is the planner's responsibility in a plan-execute agent?Define
2. Why does the planner take the execution history and last_error as inputs, not just the goal?Explain
3. After a mid-plan failure the agent re-runs three steps that had already succeeded. Which mistake in the planner is the likeliest cause?Analyze

The executor

The executor is a small state machine. Each step runs through the dispatcher and ends in one of three outcomes: success, failure-replannable, or failure-fatal. Replannable failures go back to the planner. Fatal failures (budget exceeded, replan ceiling hit) return a FAILED session result.

Mermaid diagram

Check: The executor0/2
1. Each step run through the dispatcher resolves to one of three outcomes. What are they?Define
2. In the demo run, step 2 (transform v1) fails and the planner returns steps 2, 3 and 4 with transform in mode v2. What does the final history contain?Apply

Plan diffs on revision

A plan diff is the executor's record of how a revised plan differs from the one it replaces. When the planner returns a new plan after a failure, the executor emits a plan.diff event with three fields.

text
removed: list of step ids that were in the old plan and are not in the new
added  : list of step ids in the new plan that were not in the old
revised: list of step ids whose tool_name or args changed

A tracer or UI can render this as a strikethrough on removed steps and a highlight on added ones. The diff format matters less than the principle: revision is a visible event, not a silent rewrite.

Check: Plan diffs on revision0/6
1. What is a plan diff?Define
2. When the replanner returns a new plan after a failure, what does the executor emit on the event stream?Explain
3. The demo's failure at step 2 leaves old steps 2, 3, 4 and the new plan is steps 2, 3, 4, with step 2's args changed from mode v1 to v2. What does plan.diff report?Apply
4. A step fails and the planner returns a non-empty revised plan. Which events follow that step's step.end, in order?Apply
5. A trace shows steps vanishing and new ones appearing after each failure, and nobody can tell why the plan changed. Which piece is missing?Analyze
6. You are building a UI that shows why a plan changed after each failure. Which data would you wire to it?Design

Two budgets, both hard

max_steps caps total step executions across the whole session, replans included. The default is twelve. Say a linear five-step plan fails at its last step and replans twice, and each revision retries the failed step and adds three more. That is 5 + 4 + 4 = 13 executions, because failed attempts count, so with the default budget of twelve the executor stops with FAILED (step_budget).

max_replans caps how many times the planner is called after the first plan. The default is five, and this is the more important limit. Without it, a planner that returns the same broken plan five times in a row would loop until the step budget stopped it. Capping replans makes the failure faster and the reason clearer.

Check: Two budgets, both hard0/9
1. What does max_steps count?Define
2. Why keep max_steps and max_replans as two separate budgets?Explain
3. Why does a failed attempt still count against max_steps?Explain
4. With the default budgets, what is the largest number of times the planner can be called in one session, counting the first plan?Apply
5. A linear five-step plan fails at its last step and replans twice. Each revision retries the failed step and adds three more. With default budgets, how does the session end?Apply
6. A planner returns a 20-step plan of tools that always succeed, and max_steps is 5. How does run() end?Apply
7. max_replans is 0 and the planner's only step always raises. How does run() end?Apply
8. max_replans is 2 and max_steps is 50. The first plan is [fetch, transform], every revision is [transform], and transform always raises. How many entries are in history when run() ends?Apply
9. A planner keeps returning the same broken plan. With default settings, which limit is meant to stop it, and why is that the better stop?Analyze

The deterministic planner in this lesson

This lesson calls no model. It ships a deterministic planner that picks a plan based on last_error.

text
last_error is None    -> emit a four-step plan
last_error matches X  -> emit a three-step plan that routes around X
last_error matches Y  -> emit a two-step plan that gives up gracefully
otherwise             -> return [] (signals nothing to replan)

That is enough to exercise every transition path in the executor: success, replan-once, replan-twice, replan-exhaustion, and step-budget exhaustion.

Check: The deterministic planner in this lesson0/1
1. Why does the lesson ship a deterministic planner that calls no model?Explain

Result shape

A session returns a SessionResult, which carries the outcome, the reason, and the full trail of what happened.

text
SessionResult
  status      : "completed" | "failed"
  reason      : str     ("goal_met" | "step_budget" | "replan_budget" | "no_plan")
  history     : list[Step]
  revisions   : list[PlanDiff]
  events      : list[Event]

The harness loop from lesson twenty can read this directly. The dispatcher from lesson twenty-three executes each step, and the registry from lesson twenty-one validates each step's args. The transport from lesson twenty-two would carry this whole flow over JSON-RPC to a model client.

Check: Result shape0/4
1. What is a SessionResult?Define
2. The planner returns an empty list on its very first call. Which reason does the session report?Apply
3. A session has already replanned five times, and the step it just ran fails again, all within twelve steps. What does the result show?Apply
4. A dashboard must separate sessions that ran out of steps from sessions where the planner kept failing. Which SessionResult field do you group by?Design
Author's Note

I'd build the replanner before the happy path. A plan that cannot survive a failure is a script, and the replan branch is what makes this an agent, so that is where I'd put my first tests.

I'd also set max_replans before max_steps. The lesson calls it the more important limit because capping replans makes a failure faster and its reason clearer. My bet is that the plan.diff event pays off in debugging: revision is a visible event, not a silent rewrite, and that is what I'd want in a trace when a planner keeps changing its plan.

How to read the code

code/main.py defines PlanExecuteAgent, Step, PlanDiff, SessionResult, and the deterministic planner. The executor is a single run(goal) method that returns a SessionResult. Plan diffs come from comparing step ids and (tool_name, args) tuples.

code/tests/test_agent.py covers a linear success, a mid-plan failure that replans once, replan exhaustion that returns failed:replan_budget, step-budget exhaustion, and the plan-diff event format.

Going further

Two extensions become worth building once you wire this to a real model. The first is partial-plan caching. Suppose a plan succeeds on its first three of six steps and then fails: you do not want to re-run those three. The executor already keeps history, so the planner only needs to read it. The second is parallel branches. The current executor is strictly sequential, but a planner that emits an independent branch (gather_step instead of next_step) can have two tool calls run concurrently through the dispatcher.

Both add real complexity, and both are easier to add once the linear executor is pinned down. That is the job of this lesson.

Warm-up0/3
1. In an agent loop, a tool call comes back with an error. What should the harness do with that error?Warm-up
2. Why does an agent harness cap how many tool calls one session may make?Warm-up
3. A harness has to act on model output by calling a tool. Why is structured output better than free-form prose here?Warm-up
Check: Going further0/2
1. A plan succeeds on its first three of six steps, then fails. What lets the replanner avoid re-running those three?Explain
2. Two lookups in a plan are independent and you want them to run at the same time on this executor. Which approach fits?Design

Code