Stately
PackagesAgent

Preset machines

Factories for proven agent shapes (tool loop, sequential, router, parallel, loop, supervisor, handoff) that return ordinary, inspectable agent machines.

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

Overview

@statelyai/agent/machines ships factories for common agent shapes. Each factory is a thin composition over setupAgent(...).createMachine(...).

  • The result is an ordinary machine, with the same states, guards, snapshots, and lintAgentMachine support as a machine you write yourself.
  • Executors stay separate. Presets name no SDK, so the host passes executors to runAgent.
  • Each preset is around 100 lines of states that you can read, diagram, and copy into your own project.
import { tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
import { runAgent } from "@statelyai/agent";
import { createToolLoopMachine } from "@statelyai/agent/machines";
import { createAiSdkExecutors, } from "@statelyai/agent/ai-sdk";

// Model IDs here are illustrative; substitute your provider's current models.
const models = { quick: openai("gpt-5.4-mini") };

const calculate = tool({
  description: "Evaluate an arithmetic expression.",
  inputSchema: z.object({ expression: z.string() }),
  execute: async ({ expression }) => Number(expression),
});

const machine = createToolLoopMachine({
  model: "quick",
  instructions: "Answer using the tools.",
  tools: { calculate },
  maxSteps: 5,
});

const result = await runAgent(machine, {
  input: { prompt: "What is 42 times 17?" },
  executors: createAiSdkExecutors({ models }),
});

Snapshots and log entries carry machine.version automatically. See Versioning.

Preset taxonomy

Three questions separate the seven presets:

  • Who runs the work? One request in toolLoop, a fixed chain in sequential, or named sub-units in the rest.
  • Who picks the next unit? The author in sequential, parallel, and loop. The model in router, supervisor, and handoff.
  • Does control come back? Delegation returns in supervisor. Routing ends the run in router. Handoff transfers control permanently in handoff.
PresetShapeModel choosesControl returns
createToolLoopMachineOne request, host-run tool looptoolsn/a
createSequentialMachinePrompt chain, step by stepnothingn/a
createRouterMachineOne decision picks one destinationthe routeno, the run ends there
createParallelMachineStatic fan-out, joinednothingyes, at the join
createLoopMachineBounded repeatnothingyes, each iteration
createSupervisorMachineDelegate, accumulate, repeatthe worker, or FINISHyes, every turn
createHandoffMachinePeer swarm, activeAgent names the agent that runs the turnn/a (host sends transfer_to_*)no, transfer is final

Config names

Every preset uses the same vocabulary:

OptionMeaning
modelA model ref or a models alias. A model on an entry overrides the factory default.
instructionsThe system prompt.
toolsTools the host runs inside one request.
outputSchemaStructured output. Omit it for plain text.
maxStepsThe bound on a request's host-side tool loop. It lowers to the request's typed maxSteps.
maxTurnsThe bound on a machine loop or delegation count. It is enforced by a guard.

Worker, route, branch, and agent entries are a record. The key is the entry's name, and each entry carries a description that the deciding model reads.

An entry is either an inline request, written as { description, instructions, model, outputSchema, tools }, or a child machine, written as { description, machine, input? }. Child machines are registered as actor sources, so runAgent binds their executors too.

The presets

createToolLoopMachine

One text request carries the tools, and the host runs the tool loop inside that request. maxSteps bounds the loop through the request's typed maxSteps field. The states are answering and done.

This preset is the default for tool use. It does not add machine states, and it sets no request metadata. To model an approval gate as states, eject. See Ejection.

createSequentialMachine

A prompt chain with one state per step. Each step's output feeds the next step. A step's prompt(ctx) can read { prompt, results, previous }. Without a prompt, the step's default prompt is the previous step's output.

createRouterMachine

One agent.decide picks exactly one declared route, then the machine runs it. Route events are named ROUTE_<name>. Build the string with routeEventType(name) when you send or assert these events. Undeclared routes have no event, no state, and no transition, so the model cannot take them. Add fallback to target a state when the decision fails. Without fallback, a failed decision errors the run.

createParallelMachine

Static parallel work with one region per branch. All branches run concurrently and join into a keyed results object. Dynamic fan-out remains ordinary XState actor composition.

createLoopMachine

A bounded repeat. The machine runs body, evaluates until over { prompt, iterations, results, last }, then repeats or stops. maxTurns is a guard, so the loop terminates even if until never returns true.

createSupervisorMachine

Each turn, one agent.decide picks a worker or finishes. maxTurns bounds the delegations and defaults to 6. It is a machine budget, and it never lowers a request's maxSteps. Worker events are named DELEGATE_<name>, built with delegateEventType(name). The finish event is FINISH, exported as FINISH_EVENT_TYPE. Results accumulate in context and are rendered into the next decision. When the maxTurns budget is spent, every delegate is removed from the candidate set, and a guard rejects a delegate event if one is chosen anyway.

createHandoffMachine

A peer swarm. context.activeAgent names the agent that runs the current turn. After the turn, the machine settles idle in waiting. A transfer_to_<name> event, built with transferEventType(name), changes activeAgent and re-routes. There is no final state. The conversation ends when the host stops resuming it. Persist the idle snapshot between turns.

Ejection

A preset is a starting point. When you need one more state, a human gate, a different bound, or your own typed context:

  1. Open the preset's source at src/machines/<preset>.ts. It is one small file of states.
  2. Copy it into your project and edit the setupAgent(...) call directly.
  3. Run it as before. The copied machine runs, lints, and persists the same way.

Examples to eject toward:

Versioning

Every preset machine carries version: "1", using XState's standard createMachine({ version }) prop. This is the machine's own topology version. It is unrelated to the @statelyai/agent package version. Native persisted snapshots and trace events carry that version:

const result = await runAgent(machine, { input, executors });
// result.persist().version === "1"

The versioning policy:

  • The version identifies the machine's topology, not the release that built it. Prompt wording can change without a version bump.
  • A topology change that an old snapshot cannot restore bumps the version. Migrate it with XState's machine-level migrate(snapshot, fromVersion).

The same prop works for your own machines. Set version in createMachine(...) and runAgent stamps that value instead of the structural hash, which changes on any edit.

On this page