Preset machines
Factories for proven agent shapes (tool loop, sequential, router, parallel, loop, supervisor, handoff) that return ordinary, inspectable agent machines.
Alpha:
@statelyai/agent2.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
lintAgentMachinesupport as a machine you write yourself. - Executors stay separate. Presets name no SDK, so the host passes
executorstorunAgent. - 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 insequential, or named sub-units in the rest. - Who picks the next unit? The author in
sequential,parallel, andloop. The model inrouter,supervisor, andhandoff. - Does control come back? Delegation returns in
supervisor. Routing ends the run inrouter. Handoff transfers control permanently inhandoff.
| Preset | Shape | Model chooses | Control returns |
|---|---|---|---|
createToolLoopMachine | One request, host-run tool loop | tools | n/a |
createSequentialMachine | Prompt chain, step by step | nothing | n/a |
createRouterMachine | One decision picks one destination | the route | no, the run ends there |
createParallelMachine | Static fan-out, joined | nothing | yes, at the join |
createLoopMachine | Bounded repeat | nothing | yes, each iteration |
createSupervisorMachine | Delegate, accumulate, repeat | the worker, or FINISH | yes, every turn |
createHandoffMachine | Peer swarm, activeAgent names the agent that runs the turn | n/a (host sends transfer_to_*) | no, transfer is final |
Config names
Every preset uses the same vocabulary:
| Option | Meaning |
|---|---|
model | A model ref or a models alias. A model on an entry overrides the factory default. |
instructions | The system prompt. |
tools | Tools the host runs inside one request. |
outputSchema | Structured output. Omit it for plain text. |
maxSteps | The bound on a request's host-side tool loop. It lowers to the request's typed maxSteps. |
maxTurns | The 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:
- Open the preset's source at
src/machines/<preset>.ts. It is one small file of states. - Copy it into your project and edit the
setupAgent(...)call directly. - Run it as before. The copied machine runs, lints, and persists the same way.
Examples to eject toward:
- review-tool-calls: an approval gate.
- reflection-writer: a critique loop.
- hierarchical-teams and swarm-handoff: multi-agent topologies.
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.
Related
- Read more about Agent machines, including
setupAgent, states, invokes, and guards. - Read more about Agent patterns, the full runnable example catalog.
- Read more about Persistence.