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 7: State Machines Before Agent Frameworks

O-1001 has a drafted status email and sits in awaiting_approval until tomorrow. Maya will review in the morning. If the only record of that wait is chat history in one worker’s RAM, a restart forgets the phase — and a second worker may re-draft, send early, or park the case twice.

Operators need to read stored state, not ask a model to summarize a transcript.

draft ready → awaiting_approval → (overnight) → approve → executing → send → completed

This chapter names FSM phasesplanning, awaiting_tool, awaiting_approval, retrying, terminal states — and makes legal exits explicit before any agent framework enters the picture.

First principles

A finite-state machine (FSM) makes case lifecycle inspectable and enforceable:

PhaseMeaning
idleCase exists; not started
planningChoosing next action
awaiting_toolTool call in flight
awaiting_approvalHuman gate
executingPerforming permitted side effect
retryingTransient failure path
completed / failedTerminal

For ShopOps, phase answers: Is O-1001 gathering facts? Waiting for a human? Retrying a gateway? Done? Operators read stored state—not a model summarizing a transcript.

Choose control structure to match lifecycle:

PatternUse when
FSMSmall closed lifecycle; strong invariants
Graph / workflowBranching pipelines with named nodes
Open loopExploratory tools with hard budgets + watchdogs

Framework graphs implement transitions; they do not choose business states for you. If you cannot name what a case waits for, you cannot operate it.

Concrete example

AgentFSM drives ShopOps case lifecycle. Overnight, the case sits in awaiting_approval. Morning approval fires approveexecuting.

Diagram

stateDiagram-v2
  [*] --> idle
  idle --> planning: start
  planning --> awaiting_tool: tool_call
  awaiting_tool --> planning: tool_ok
  awaiting_tool --> retrying: tool_retry
  retrying --> awaiting_tool: tool_call
  planning --> awaiting_approval: propose_send
  awaiting_approval --> executing: approve
  awaiting_approval --> planning: reject
  executing --> awaiting_tool: tool_call
  planning --> completed: complete
  executing --> completed: complete
  awaiting_approval --> completed: complete
  idle --> failed: fail
  planning --> failed: fail
  awaiting_tool --> failed: fail
  awaiting_approval --> failed: fail
  executing --> failed: fail
  retrying --> failed: fail

Notice: approve from planning is illegal — tests should say so.

Implementation

PYTHONPATH=. python examples/ch07_fsm.py

shopops/fsm.py:

fsm = AgentFSM()
fsm.handle("start")          # idle → planning
fsm.handle("tool_call")      # → awaiting_tool
fsm.handle("tool_ok")        # → planning
fsm.handle("propose_send")   # → awaiting_approval
# ... overnight ...
fsm.handle("approve")        # → executing

Illegal transitions raise IllegalTransition — do not coerce.

Wire FSM phase into State.phase (Ch 2) so checkpoints (Ch 8) persist the same noun ops dashboards show.

Overnight approval as a product feature

Order support is not a chat demo. Humans batch reviews. O-1001 may sit in awaiting_approval for hours. That is success:

  • No worker thread held open
  • No busy-loop polling the model
  • No accidental send at 2am because a retry woke up

The FSM makes that park visible in SQL: WHERE phase = 'awaiting_approval'. Chat-history control flow cannot answer that query without brittle NLP.

Illegal transitions are tests you want

fsm = AgentFSM()
fsm.handle("start")
# cannot approve before proposing a send
with pytest.raises(IllegalTransition):
    fsm.handle("approve")

Every illegal edge you don’t test becomes a silent coercion in some future refactor (“just set phase = executing”). Coercion is how duplicates and skipped policy checks appear.

Open loop still has a machine

Even if you keep a ReAct-style open loop for planning (Part IV), the case lifecycle can remain an FSM. Micro decisions may be open; macro status should not be. That hybrid is normal: open loop inside planning, FSM around the case.

Failure modes

Phantom states in prose. Transcript says “waiting on manager”; FSM still planning; a second worker sends.

Catch-all transitions. Everything can go to everywhere — FSM becomes décor.

Framework opacity. Generated graphs with unnamed nodes; on-call cannot answer “what is this case waiting on?”

Dual sources of truth. FSM says awaiting_approval, boolean awaiting_approval disagrees.

Retry as a vibe. No retrying phase; transport retries hide from operators.

Production considerations

  • Emit phase as a first-class metric/label on every case.
  • Persist phase in checkpoints; resume must reload FSM, not infer from last message.
  • Property tests: random legal event sequences never enter undefined states; illegal events always throw.
  • Map FSM names to any later framework node names in a glossary — one concept, two spellings.
  • Durable execution systems (e.g. Temporal workflows) rhyme with this: explicit status, replay, signals for approval. [VERIFY SOURCE: Temporal docs conceptual]

Chapter summary

  • Explicit FSM phases beat chat-inferred control flow.
  • O-1001 parks in awaiting_approval overnight on purpose.
  • Illegal transitions must fail loudly.
  • FSM vs graph vs open loop is a design choice with trade-offs.
  • Frameworks map to states; they do not invent discipline.
  • Align State.phase with the FSM.
  • Retrying is a named phase, not only a sleep().
  • Code: shopops/fsm.py, examples/ch07_fsm.py.

Exercises

  1. Write tests for every illegal transition out of awaiting_approval except approve, reject, complete, fail.
  2. Map each FSM phase to a hypothetical LangGraph node name (concept-only table).
  3. Add a cancel event from awaiting_approvalfailed with reason cancelled_by_ops.
  4. Fuzz 1_000 random event sequences filtered to legal moves; assert no crash and phase always in the enum.

References

  • Standard FSM / statechart teaching material (Hopcroft/Ullman or lighter tutorials). [VERIFY SOURCE for preferred citation]
  • Temporal documentation — workflows, signals, determinism (conceptual overlap). [VERIFY SOURCE]
  • Kleppmann, 2017 — application state vs message logs when you later add ledgers (Ch 16).