Curriculum/Agent Architectures
State Machines for Agents
Named states, allowed tools per state, and guards on transitions. Graphs you can draw beat loops you cannot.
A state machine is typed state plus explicit transitions:
- From
gather,get_ticketmay move you toapplyonly if the ticket is open - From
apply,refundmay move you todoneonly if amount ≤ cap - Unknown transitions are errors, not improvisation
Typed state gave you a dict. The machine gives you edges. Allowed tools are not enough. Guards check the world before the phase changes. Refund in gather is illegal even if the model is sure. Open ticket with amount 80 fails the apply guard even if refund is a legal name in apply.
If you cannot draw the graph on a whiteboard, operators cannot debug 3 a.m. runs. Draw: gather → apply → done, plus handoff loops that stay put, plus search that stays in gather.
Search stays in gather. Refund may move apply to done only if the guard passes.
Allowed edges, not vibesLangGraph-style node graphs are this idea with nicer furniture. The furniture is not the architecture. If the library cannot express “refund illegal in gather,” you still write the allowlist.
Allowed vs guard vs next
Three tables:
- Allowed — which tool names may run in this state
- Guards — extra predicates on the observation (or on args) before a transition fires
- Next — where you go if allowed and guard pass
Search is allowed in gather and next is gather (self-loop). Get_ticket is allowed in gather; guard is status open; next is apply. Refund is not in gather’s allowed list — you never consult a guard. Amount 80 in apply: name allowed, guard fails, state does not change.
Failing a guard is not “try a nearby tool.” It is guard failed as an observation. The model may try a different path. Stop still caps. Do not auto-jump to done.
Unknown transitions are errors
If the model emits a tool that has no row, that is illegal tool. If you forgot to add a next mapping, fail closed — do not default to done. Missing next is a programmer bug. Defaulting to stay is safer than defaulting to success.
Handoff often stays in the same state and stops the loop. Done has an empty allowlist. Those are explicit.
Draw it, then test it
Tests can fire illegal edges with no tokens: transition("gather", "refund", {}) must not become apply. transition("gather", "get_ticket", {"status": "closed"}) stays gather. Open ticket moves. Amount 12 refunds to done. Amount 80 stays apply.
That is cheaper than hoping the model “knows.” Put the graph in config (dicts), not only in a prompt. The prompt may mention the phases. Code enforces them.
Guards read the world
Guards should read observations and typed fields, not thoughts. obs.get("status") == "open" is a guard. “The thought said it was open” is not. Caps on amount belong here and in the tool policy table (tools track). Duplicate the cap if you must. Do not leave it only in English.
Graphs you can operate
Operators should ask “which node?” not “what was the vibe?” The trace logs state before, tool, guard result, state after. When money moved, you should see apply → done on an allowed edge with a passing guard. If you see gather → done, you have a bug in next maps, not a clever model.
What this is not
This is not a multi-agent workflow of people. It is one agent’s phase graph. Multi-agent next track may use graphs between specialists. You still need this graph inside a specialist. Skip it and the swarm will refund in gather with extra hops.
This is not production orchestration (queues, k8s). Jobs later load this state. The machine is the logic. The worker is the runner.
Self-loops, illegal edges, and drawing the picture
Search that stays in gather is a self-loop: allowed, no phase change, still costs a step. You still append an observation. You still check thrash. A self-loop is not “free thinking.” Handoff that stays in apply is a self-loop plus a stop: phase remains so a human can resume the same node.
Illegal edges should be noisy. Return illegal tool refund as the message and as an observation. Silent ignore looks like the model “did nothing” and it will retry the same edge until budget. Pair with thrash. Tests should include at least one illegal edge per node, one failed guard per guarded edge, and one happy path across the whole graph.
Draw it: boxes for gather / apply / done, arrows labeled with tool names, notes on arrows for guards (“status open”, “amount <= 50”). If you cannot draw it in five minutes, the graph is too clever or it lives only in a prompt. Operators at 3 a.m. get the drawing, not your memory of the LangGraph blog.
Config shape is three dicts (allowed, guards, next) or one list of edges {from, tool, guard, to}. Either is fine. What is not fine is “the model will figure out the phase.” Next maps that default to done are how gather plus a typo pays money. Prefer default stay + error.
Guards can read typed state as well as obs: “refund only if ticket_id is not null.” That is still the world (you stored the id from a prior obs). They must not read the thought string.
Common mistakes
- Allowed names without guards.
- Guards that read thoughts.
- Default next = done.
- Graph only in the prompt.
- Cannot draw it.
- Empty error on illegal tool so the model retries the same edge forever (pair with thrash later).
Run to execute this in your browser. Nothing is sent to a server.
Refund in gather is illegal. Closed ticket fails the guard. Open ticket moves to apply. Amount 80 fails the apply guard; amount 12 moves to done. Read the tuples: state plus message. Unchanged state plus guard failed is a successful control outcome. The world did not lie; the machine refused.
The default amount in the refund guard is 999, so a missing amount fails the cap. Fail closed on missing money fields.
How agents use this
Put the graph in config, not only in a prompt. Tests can fire illegal edges with no tokens. That is cheaper than hoping the model “knows” not to refund in gather.
HITL is a special kind of stay: you leave phase as apply, set pending_approval, and stop the worker until a human resumes. The edge to done does not fire until approve. Next two lessons.
Check your understanding