Plans
Iterate a decision against the live snapshot, applying legal events one at a time until the model decides to stop.
Alpha:
@statelyai/agent2.0 is in alpha. APIs can change between releases; pin an exact version. Feedback: github.com/statelyai/agent.
Overview
A decision is one event. A plan is many: the agent.plan builtin iterates a decision against the live snapshot, applying legal events one at a time until it decides to stop. Use it when a single command maps to several events ("add X and Y" → two ADD_TODO).
Every step gets the same legality guarantees as a single decision: candidates come from the machine, guards reject illegal choices, rejected choices retry with typed feedback.
Authoring
Author it inline with src: 'agent.plan', same shape as agent.decide plus stopOn and maxSteps:
planning: {
invoke: {
src: 'agent.plan',
input: ({ context }) => ({
model: 'quick',
prompt: renderCommand(context),
allowedEvents: ['ADD_TODO', 'TOGGLE_TODO', 'QUIT'],
maxSteps: 8, // hard cap (default 8)
}),
onDone: { target: 'awaitingCommand' }, // output: { steps, stopped }
},
on: {
ADD_TODO: ({ context, event }) => ({ context: { /* ... */ } }),
QUIT: { target: 'done' }, // exiting the state ends the plan (see below)
},
}allowedEvents accepts the same exact-type and wildcard forms as agent.decide; see allowedEvents patterns.
How it behaves
- Iterated decide. Each step re-reads the live snapshot, so candidates reflect everything applied so far, and runs the same validation/retry loop a single decision gets. A guard-rejected step retries with
rejected-by-guardfeedback (see Validation and retries). - Built-in done move. Every step is offered a reserved
agent.plan.donecandidate (PLAN_DONE_EVENT_TYPE). Choosing it ends the plan (stopped: 'done') and is never sent to the machine, so the machine needs no no-op sentinel of its own. The library hints this move in the prompt automatically. maxStepscaps the plan (default 8). Also stops when no legal candidate remains.- Applied trail is appended to the prompt automatically each step, so the model sees progress without you threading it through context.
- Exiting the state ends the plan. An applied event that leaves the invoking state (e.g.
QUITabove) cancels the invoke; xstate moves on andonDonenever runs. - Partial application, no rollback. Events are sent as the plan runs, not staged. If step 3 of 5 stops the plan, steps 1–2 stay applied. No transactional undo.
onDoneoutput is{ steps, stopped }: the events applied in order, and why the loop ended ('done' | 'stop-event' | 'max-steps' | 'no-legal-events').stopOn(rare). For "send this real machine event and stop", list it instopOn: the event is validated and sent, then the loop ends (stopped: 'stop-event'). The built-in done move covers the common "no further action" case, sostopOnis only for ending on an actual state change.- One ledger, both hosts.
agent.planis a single stateful, transition-based ledger actor: its snapshotcontext({ applied, stepsRemaining, stopped }) holds the in-progress plan state, advanced oneplan.applied/plan.endedevent at a time.runAgentdrives it directly; the step path surfaces it as a re-surfacingkind: 'plan'request thatresolveAgentRequestsadvances one step per call. Both hosts drive the same ledger through shared drivers, and because the ledger is the invoke child's owncontext, it lands atchildren.<id>.snapshot.contextin a persisted snapshot for free, surviving persist/resume mid-plan.
Full example: examples/todo-nl/index.ts drives free-text commands through one agent.plan invoke.
Where to go next
- Decisions: the single-event form, validation, retries, and coercion.
- The step path: plans as re-surfacing step requests with crash-safe mid-plan resume.
- Verify: plan branches in model-free simulation.