Stately
PackagesAgent

Decisions

Let the model choose one currently-legal machine event, validated and retried by the machine before it is taken.

Alpha: @statelyai/agent 2.0 is in alpha. APIs can change between releases; pin an exact version. Feedback: github.com/statelyai/agent.

Overview

A decision lets the model choose an event to send to the running machine. It chooses based on which events are enabled in the current state: the machine declares candidate events, guards decide which are legal, and the model picks among the survivors. Not free text, not an arbitrary tool call; an out-of-bounds choice is impossible, not merely discouraged by a prompt.

The snippets below are from Twenty Questions, where each turn the model chooses ASK or GUESS.

Invoking an agent decision

Author a decision inline with the builtin agent.decide actor source, on the invoke that needs one. Its input takes:

  • model: which model to use (a key from your models map).
  • system (optional): system prompt.
  • prompt (optional): user prompt, usually built from context.
  • allowedEvents (optional): the candidate events (exact types or patterns). Defaults to all currently-legal events.
  • maxRetries (optional): retries after an invalid choice. Default 2.
// ...
deciding: {
  invoke: {
    id: 'chooseAction',
    src: 'agent.decide',
    input: ({ context }) => ({
      model: 'quick',
      system: 'Ask one yes/no question at a time, but guess on the final turn.',
      prompt: `Questions remaining: ${context.questionsRemaining}`,
      allowedEvents: ['ASK', 'GUESS'],
    }),
    // The model never made a valid choice (retries exhausted).
    onError: { target: 'failed' },
  },
  on: {
    ASK: ({ context, event }) =>
      context.questionsRemaining > 1 ? { target: 'awaitingAnswer', context: { /* ... */ } } : undefined,
    GUESS: ({ context, event }) => ({ target: 'revealing', context: { guess: event.guess } }),
  },
}
// ...

The allowedEvents list is strongly typed against the machine's event schema, so a typo is a compile error. Listing events explicitly also makes the candidate set reviewable in the machine.

Note: agent.decide needs a snapshot-aware host (runAgent or the step path) to know which events are currently legal. Under a bare createActor(...), list allowedEvents explicitly; wildcards and the omitted default cannot expand there.

Note: allowedEvents narrows the declared candidates; guards then decide what is actually legal from the current snapshot. A declared-but-currently-illegal choice does not get through.

allowedEvents patterns

The allowedEvents option (on both agent.decide and agent.plan) accepts a single string or an array. Entries are exact event types or wildcard patterns, and the two can mix (['todo.*', 'reset']):

  • ['ASK', 'GUESS']: exact types, typed against the event-schema keys (typo = compile error).
  • 'ASK': a single string, shorthand for a one-entry array.
  • '*': every currently-legal event.
  • 'todo.*': a dotted namespace, every declared event under todo. (todo.add, todo.toggle, …). Typed against declared dotted types, so 'nope.*' (matching nothing) is a compile error.

Delivering the chosen event

Delivery is automatic: when the decision resolves, the agent.decide actor sends the chosen event to the machine, and the matching on: transition runs. You handle the outcome with ordinary transitions, no special decision plumbing.

Note: The chosen event's transition typically exits the invoking state, cancelling the invoke, so onDone normally never fires (same as agent.plan). Declare onDone only when the chosen event's transition stays in-state; the invoke then completes with the chosen event as output. onError (retries exhausted, DecisionExhaustedError) is unaffected.

Guard enforcement

A candidate event's guard is its transition function returning undefined (see transitions). Before accepting a choice, the library checks snapshot.can(event), so a chosen ASK on the final turn (guard returns undefined) is rejected and the model asked again. The machine, not the prompt, is the source of truth for legality.

Guards may read the event payload, so candidates cannot be filtered upfront: a decision offers the full allowedEvents set (intersected with what the state statically accepts), and legality is checked after the model picks. runAgent and the step path handle this for you. When calling resolveDecision directly (uncontrolled mode), thread the check via canTake:

const event = await resolveDecision(request, executors.decide, {
  canTake: (e) => snapshot.can(e),
});

Validation and retries

Each attempt runs three checks in order. Each failure is typed and fed back to the model on the next attempt:

  • unknown-event: the type is not among the candidate events.
  • invalid-payload: the payload does not match that event's schema.
  • rejected-by-guard: type and payload are fine, but snapshot.can(event) returned false.

Retry behavior:

  • Default 2 retries, so up to 3 attempts. Set maxRetries on the decide input to change it.
  • Prior failed attempts ride on request.attempts, so the host can render "your last choice failed because X" into the next call. Core never rewrites the prompt itself; see Hosts.
  • Exhausting retries throws DecisionExhaustedError (carrying the attempts list), caught by the invoke's onError.

Coercion

Core validates and retries; it never talks to a model. Coercing the model into choosing exactly one option (tool-per-event with forced tool choice, structured output over an event union, etc.) is the host's responsibility. The shipped createAiSdkExecutors provides a decide executor for the Vercel AI SDK; the raw-SDK examples force the choice with tool_choice. See Hosts.

Note: Decisions are state-local: author them inline on the invoke. There is no reusable decision-logic object, because a decision's candidates and legality depend on the state it runs in.

Plans (multi-event decisions)

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 the model decides to stop. Every step gets this page's validation/retry loop. See Plans.

Where to go next

  • Agent machines: transitions, guards, and event schemas.
  • Plans: the multi-event form, agent.plan.
  • Hosts: the decide executor and how the model is coerced into one event.
  • Machines as data: authoring decisions from JSON.

On this page