Chapter 11 — Planning Without Mysticism
O-1001 is delayed. Before Outreach contacts Jordan, ShopOps needs customer facts, order status, and a draft that matches both. A model that picks the next tool from a prompt alone may call draft_email before get_customer returns preferred_channel — and still produce convincing text Policy will block at send for missing required fields.
Planning is structured action selection, not harder thinking.
plan: get_customer → get_order → draft_email → (replan if observation falsifies step)
This chapter makes the plan a data structure — goal, steps, cursor, horizon, replan trigger — so dependencies are executable: missing field stops the plan instead of becoming a model guess.
First principles
A plan is a data structure, not a narrative:
- Goal — terminal condition (“compliant first-contact draft ready”).
- Steps — ordered actions with expected observation shape.
- Cursor — which step is live.
- Horizon — how far ahead you commit before checking reality.
- Replan trigger — when observations falsify expectations.
For O-1001, a plan can require get_customer to return preferred_channel before draft_email runs. That expectation is executable: missing field → runtime repairs or stops the plan, not a model guess.
Compare three policies on the same CaseState:
| Policy | Commits | Cost | Brittle when |
|---|---|---|---|
| Direct action | One tool/answer per call | Lowest latency | Multi-step dependencies |
| Explicit plan | A sequence up front | Plan tokens + execute | World changes mid-plan |
| HTN-style decompose | Goal → subgoals → primitives | Extra structure | Bad decomposition library |
ReAct (next chapter) is the reactive extreme: horizon ≈ 1. Production ShopOps usually lives in the middle: short explicit plans with hard replan budgets.
The control-loop invariant still holds:
[ s_{t+1} = F(s_t, a_t, o_{t+1}) ]
A plan only proposes candidate actions. Observations still win. If get_customer returns without preferred_channel, the step fails—you do not infer a channel and continue.
Concrete example
Multi-day product plan (contact tomorrow, reminder in three days) is not the same as a runtime plan. Runtime plans should be short enough to invalidate:
- Load customer facts (
get_customer) - Load order
- Draft email (never send from the planner)
Sending is a later stage behind policy and approval (Ch 5, 28–29). The planner’s job is to assemble evidence and a draft artifact.
Diagram
flowchart TB
subgraph reactive [Reactive loop horizon=1]
R1[decide] --> R2[act] --> R3[observe] --> R1
end
subgraph planned [Explicit plan]
P[Plan tree] --> S1[step 1]
S1 --> S2[step 2]
S2 --> S3[step 3]
S2 -.->|mismatch| RP[replan]
RP --> S2b[repair step]
S2b --> S3
end
Notice: replanning is a first-class edge, not an embarrassment. Plans that cannot fail closed will invent success.
Implementation
Reference module: shopops/planning.py.
from shopops.planning import (
make_order_outreach_plan,
run_plan,
)
from shopops.types import Action, CaseState, Observation
def execute(action: Action) -> Observation:
if action.tool == "get_customer":
return Observation(
ok=True,
data={"customer_id": "cust-O-1001", "preferred_channel": "whatsapp"},
)
if action.tool == "get_order":
return Observation(
ok=True,
data={"order_total_cents": 240_00, "days_since_shipped": 32},
)
if action.tool == "draft_email":
return Observation(ok=True, data={"draft_id": "d-9", "body": "..."})
return Observation(ok=False, error="unknown")
state = CaseState(case_id="O-1001", order_total_cents=240_00)
plan = make_order_outreach_plan("O-1001", state.order_total_cents)
plan, state, status = run_plan(plan, state, execute, max_replans=2)
assert status == "done"
assert plan.done
Each PlanStep carries expect_keys. observation_matches is boring on purpose: if the keys are missing, the step is FAILED, and replan_on_failure inserts a repair step (re-run get_customer) rather than skipping ahead. max_replans is a budget, same family as max_steps in the control loop (Ch 1–2).
ASCII view of one repair:
v1: [get_customer] [get_order] [draft_email]
| fail (no preferred_channel)
v2: [get_customer DONE] [repair:get_customer] [get_order] [draft_email]
^ cursor
Failure modes
- Plan forever — model emits a 40-step novel every turn. Cap plan length and replan count in code.
- Success fiction — treat natural-language “done” as step completion without schema checks.
- Stale plan — execute a plan checkpointed yesterday against today’s send window; always re-check policy at act time (Ch 5).
- Planner sends — if the plan vocabulary includes
send_email, you have already lost separation of duties. Draft ≠ send. - Replan thrash — alternating repairs burn tokens. After
max_replans, escalate.
Production considerations
- Persist the
Planobject next to the case checkpoint (Ch 8). On resume, re-validate the current step’s preconditions; do not blindly continue. - Measure extra tokens from planning vs direct action on the golden path. If planning does not reduce illegal sends or retries, it is décor.
- Prefer library decompositions (“first_contact_pack”) over free-form HTN invented by the model each run. Closed vocabulary of plan templates scales; open-ended plan poetry does not.
- Latency: planning adds a model call (or a template lookup). For hot paths that are fully enumerable, use a rules planner and save the LM for residual cases.
Direct action vs plan vs product schedule
Keep three horizons mentally separate:
- Direct action — one tool now (
get_order). - Runtime plan — 2–5 steps inside one episode, invalidated by observations.
- Product schedule — multi-day touches stored as case workflow data (CRM tasks), advanced by the orchestrator/cron — not by a 40-step LM plan living in chat.
ShopOps “multi-day rerefund or reship” marketing language maps to (3). The agent episode that prepares tonight’s draft maps to (2). Confusing them produces either brittle mega-plans or agents that cannot commit to tomorrow’s reminder because they never wrote a durable schedule row.
Chapter summary
- Planning is structured action selection with explicit expectations and replan triggers.
- Horizon is a trade-off: 1 ≈ ReAct; long open plans rot.
- Steps fail on observation mismatch — never on vibes.
- ShopOps outreach plans draft; they do not send.
- Replan budgets are control-loop budgets.
- Templates beat improvised mega-plans in regulated domains.
- Persist and re-validate plans across checkpoints.
- If planning does not change safety or cost metrics, delete it.
Exercises
- Force
get_orderto omitdays_since_shipped. Confirmrun_planreplans or fails closed; measure step count vs happy path. - Add a fourth template step
check_opt_outwithexpect_keys=["opted_out"]. Wire a mock tool. - Design (on paper) when ShopOps should use direct action vs a 3-step plan vs an overnight multi-touch product schedule stored outside the agent loop.
- Break the planner: allow
send_emailinmake_order_outreach_plan, then write the policy test that must fail in CI.
References
- Yao et al., 2022. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 — reactive extreme of the planning spectrum.
- Classical planning / HTN survey excerpts for vocabulary (task, method, primitive). [VERIFY SOURCE — pick a standard AI planning textbook chapter; do not invent page claims.]
- Anthropic. Building Effective Agents. 2024 — workflow vs agent framing. [VERIFY URL]