Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

PolicyCommitsCostBrittle when
Direct actionOne tool/answer per callLowest latencyMulti-step dependencies
Explicit planA sequence up frontPlan tokens + executeWorld changes mid-plan
HTN-style decomposeGoal → subgoals → primitivesExtra structureBad 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:

  1. Load customer facts (get_customer)
  2. Load order
  3. 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

  1. Plan forever — model emits a 40-step novel every turn. Cap plan length and replan count in code.
  2. Success fiction — treat natural-language “done” as step completion without schema checks.
  3. Stale plan — execute a plan checkpointed yesterday against today’s send window; always re-check policy at act time (Ch 5).
  4. Planner sends — if the plan vocabulary includes send_email, you have already lost separation of duties. Draft ≠ send.
  5. Replan thrash — alternating repairs burn tokens. After max_replans, escalate.

Production considerations

  • Persist the Plan object 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:

  1. Direct action — one tool now (get_order).
  2. Runtime plan — 2–5 steps inside one episode, invalidated by observations.
  3. 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

  1. Force get_order to omit days_since_shipped. Confirm run_plan replans or fails closed; measure step count vs happy path.
  2. Add a fourth template step check_opt_out with expect_keys=["opted_out"]. Wire a mock tool.
  3. 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.
  4. Break the planner: allow send_email in make_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]