Skip to content
Chapter 5 of 13

Planning and control

Narrated walkthrough · Planning and control
A loop needs a control strategy, and three of them are worth knowing cold. ReAct decides one step at a time, reason then act then observe. It is the simplest thing that works and it is also the thing that wanders: with no record of what is left to do, it can re-send the same failing step until the cap stops it. Plan-then-execute spends one call up front decomposing the ask into ordered sub-steps and commits to that order, which costs it the ability to adapt mid-run but buys something ReAct cannot offer, a plan a human can read before anything happens. Reflect-and-retry adds a critic pass after each result, so it costs more calls than either of the others, and it is the only one of the three that catches its own mistakes. The plan is not a thought, it is state: written into the transcript, it is something the agent re-reads on step four to recall what step five was for. The lab runs one multi-step task under a bare ReAct loop and under plan-then-execute, then verifies the answer against independently computed evidence instead of the agent's own claim of success.

The lab: read it, then run it

labs/agentic-engineering/ae5-plan-then-execute.py
#!/usr/bin/env python3
"""
LAB AE5: Plan-then-execute versus a bare ReAct loop.

A ReAct loop decides one step at a time and keeps no record of what is left to
do, so on a task with ordered dependencies it can re-observe the same thing
forever. Plan-then-execute writes the ordered sub-steps down FIRST, which costs
one extra call and buys two things: the loop always knows what remains, and a
human can read the plan before anything happens. Then a VERIFICATION LOOP checks
the answer against independently computed evidence instead of trusting the
agent's own claim of success. Same task, both strategies, in one run.

Run: python3 modules/academy-content/labs/agentic-engineering/ae5-plan-then-execute.py
"""
import sys, os
_cands = [os.path.join(os.path.dirname(__file__), "..") if "__file__" in globals() else None,
          os.path.join(os.getcwd(), "..", "labs"), os.path.join(os.getcwd(), "labs")]
for _c in _cands:
    if _c and os.path.exists(os.path.join(_c, "academy_llm.py")):
        sys.path.insert(0, os.path.abspath(_c)); break
from academy_llm import tool_route
import re

INVOICES = [120, 340, 90]           # the facts on the table
TASK = "add invoices 120 and 340 and 90 then take 10 percent of the total"


def calculator(expr, register=None):
    """Arithmetic on ONE binary expression. `$R` means "the last result", which
    is how a planned step refers to the step before it."""
    if register is not None:
        expr = expr.replace("$R", str(register))
    m = re.search(r"(-?\d+(?:\.\d+)?)\s*percent\s+of\s+(-?\d+(?:\.\d+)?)", expr, re.I)
    if m:
        return float(m.group(1)) / 100.0 * float(m.group(2))
    m = re.search(r"(-?\d+(?:\.\d+)?)\s*([+\-*/x])\s*(-?\d+(?:\.\d+)?)", expr)
    if not m:
        raise ValueError("cannot parse %r" % expr)
    a, op, b = float(m.group(1)), m.group(2), float(m.group(3))
    return {"+": a + b, "-": a - b, "*": a * b, "x": a * b, "/": a / b if b else 0.0}[op]


# --- STRATEGY 1: bare ReAct. One decision per step, no record of what remains.
print("STEP 1: bare ReAct on the whole ask, 4 steps max")
react_obs, seen = None, []
for step in range(4):
    tool = tool_route(TASK, ["calculator", "search", "weather"])
    try:
        react_obs = calculator(TASK)        # the tool needs ONE expression, not three
    except ValueError as e:
        react_obs = "error: %s" % e
    seen.append(str(react_obs))
    print(f"  step {step + 1}: tool={tool} observation={react_obs}")
# Nothing recorded which sub-step comes next, so every step re-sends the whole
# task and gets the identical observation back. That is wandering, not progress.
react_wandered = len(set(seen)) == 1 and react_obs != 55.0
print(f"  spent 4 steps, produced {react_obs!r}, needed 55.0 -> wandered: {react_wandered}")

# --- STRATEGY 2: plan-then-execute. Write the ordered plan BEFORE acting.
print("")
print("STEP 2: plan-then-execute, plan written before any action")
plan = [f"calculator: {INVOICES[0]} + {INVOICES[1]}",
        "calculator: $R + %d" % INVOICES[2],
        "calculator: 10 percent of $R"]
for i, line in enumerate(plan, 1):       # the plan is state, printed and re-readable
    print(f"  plan[{i}] {line}")
actions_taken = 0
register = None
for i, line in enumerate(plan, 1):
    expr = line.split("calculator:", 1)[1].strip()
    register = calculator(expr, register)
    actions_taken += 1
    print(f"  exec plan[{i}] -> {register}")
plan_answer = register

# --- VERIFICATION LOOP: check against evidence, do not take the agent's word.
print("")
print("STEP 3: verify the answer against independently computed evidence")
evidence = 0.10 * sum(INVOICES)
verified = abs(plan_answer - evidence) < 1e-9
print(f"  agent claims {plan_answer}, evidence says {evidence} -> verified: {verified}")

plan_auditable = actions_taken == len(plan)   # every action maps to a plan line
print("")
print(f"bare ReAct reached the answer      : {react_obs == 55.0}")
print(f"bare ReAct wandered instead        : {react_wandered}")
print(f"plan was written before acting     : True ({len(plan)} steps)")
print(f"every action mapped to a plan line : {plan_auditable}")
print(f"answer verified against evidence   : {verified}")

ok = react_wandered and plan_auditable and verified and plan_answer == 55.0
print("")
print(f"PLAN-THEN-EXECUTE WAS AUDITABLE AND CORRECT: {'YES' if ok else 'NO'}")
if not ok:
    sys.exit(1)
print("The plan is state the agent re-reads. Next: the context window it all lives in.")
Runnable lab
ae5-plan-then-execute.py

Run one multi-step task under a bare ReAct loop and under plan-then-execute, then verify the answer against independently computed evidence.

Proves: PLAN-THEN-EXECUTE WAS AUDITABLE AND CORRECT: YES

Open the notebook

Runs in your browser via Pyodide. First run loads the runtime once; no install, no server.

The bare ReAct loop burned all four of its steps on the same observation. Nothing in it recorded which sub-step came next, so every step re-sent the entire task and got the identical error back, which is what wandering looks like from the outside: activity, no progress. The planned run wrote three lines first, executed each against a result register, and landed on 55.0, and every action it took maps back to a numbered plan line. That mapping is what auditable means in practice. Then the verification loop recomputed ten percent of the invoice total independently and compared the two numbers, which is the step engineers skip most often. Notice what the plan cost: three more lines of text living in the transcript, which is context the loop now carries on every later call. That resource is what the next chapter is about.
Check your understanding
  1. 1. What does plan-then-execute buy that a bare ReAct loop does not?

  2. 2. Which strategy costs the most model calls, and what does that buy?

  3. 3. Why does a verification loop check the result against independent evidence?