statelyai/agent
Create state-machine-powered LLM agents using XState
Stately Agent – Safe, State‑Machine‑Driven AI Agents
What it is
- A TypeScript/JavaScript library that lets you build LLM‑powered agents as XState state machines. The machine owns the control flow; the language model only proposes events that the machine validates.
Why it matters
- Guarantees that an agent can never take an invalid action because every possible transition is defined and guarded in code.
- Makes agents deterministic, inspectable, resumable, and testable without needing a live API key.
- Works in any JavaScript runtime (Node, Vercel, browsers) and can be paired with the Vercel AI SDK, OpenAI SDK, or TanStack AI.
Core concepts
| Concept | What it does |
|---|---|
| Machine definition | You describe the agent’s states, guards, and typed requests using XState. |
| Model proposals | The LLM is asked to produce an event (e.g., AUTO_REFUND). |
| Guard validation | The state machine checks the event against its guards; illegal events are rejected and the model is asked again. |
| Executors | Plain functions (decide, generateText, …) that call the chosen AI provider. They are supplied by the host, not baked into the library. |
| Environment & policy | Business rules live as TypeScript code (environment). A JSON‑encoded policy can be edited by a model, but it is gated by verifyAgentConfig before promotion. |
| Persistence & durability | Uses XState’s snapshot API; you can store snapshots anywhere (files, databases, durable objects). |
Quick start (refund‑request example)
import { runAgent, setupAgent } from "@statelyai/agent";
import { z } from "zod";
const agentSetup = setupAgent({
context: z.object({ request: z.string(), amount: z.number() }),
input: z.object({ request: z.string(), amount: z.number() }),
output: z.object({ outcome: z.enum(["refunded", "review"]) }),
events: {
AUTO_REFUND: {},
REVIEW: z.object({ reason: z.string() })
}
});
const refundMachine = agentSetup.createMachine({
context: ({ input }) => input,
initial: "deciding",
states: {
deciding: {
invoke: { src: "agent.decide", input: ({ context }) => ({
model: "fast",
system: "Choose AUTO_REFUND for eligible requests. Otherwise choose REVIEW.",
prompt: `${context.request}\nAmount: $${context.amount}`,
allowedEvents: ["AUTO_REFUND", "REVIEW"]
}) },
on: {
AUTO_REFUND: ({ context }) =>
context.amount <= 100 ? { target: "refunded" } : undefined,
REVIEW: { target: "review" }
}
},
refunded: { type: "final", output: () => ({ outcome: "refunded" }) },
review: { type: "final", output: () => ({ outcome: "review" }) }
}
});
// Mock executor – no API key needed
const result = await runAgent(refundMachine, {
input: { request: "I was charged twice", amount: 75 },
executors: { decide: async () => ({ event: { type: "AUTO_REFUND" } }) }
});
if (result.status === "done") console.log(result.output); // → { outcome: 'refunded' }
The model can only emit AUTO_REFUND or REVIEW. The guard on AUTO_REFUND enforces the $100 limit, so illegal choices are automatically rejected.
Installation
# Core library + XState (alpha) + Zod for schema validation
pnpm add @statelyai/agent@alpha xstate@6.0.0-alpha.64 zod
# Optional AI‑SDK executor (Vercel AI SDK)
pnpm add ai@^7 @ai-sdk/openai@^4
# Or raw OpenAI SDK executor
pnpm add openai
# Or TanStack AI executor
pnpm add @tanstack/ai @tanstack/ai-openai
Requires Node 22.18+.
How it works (architecture)
flowchart LR
M[Agent Machine<br/>states·guards·requests] -->|request| R[runAgent]
R -->|executor call| E[Host executors<br/>generateText·streamText·decide]
E -->|API call| L[LLM provider]
L -->|result| E
E -->|result| R
R -->|event / output| M
- Machine defines what the agent can do.
runAgentinvokes the machine and hands control to executors.- Executors call the chosen LLM (via AI SDK, OpenAI SDK, etc.).
- The LLM returns an event; the machine validates it against its guards.
- If valid, the machine transitions; otherwise the model is asked again.
Key features (as listed in the README)
- Typed contracts –
zodschemas for context, input, output, and events ensure compile‑time safety. - Model‑agnostic – plug any provider via executors; the core never imports an AI SDK.
- Policy‑in‑JSON – a model can rewrite a JSON policy, but
verifyAgentConfiggates promotion based on held‑out test gains. - Durable snapshots – XState’s native snapshot format lets you persist and restore agents anywhere.
- Built‑in patterns – ready‑made ReAct, plan‑and‑execute, RAG, supervisor, etc., as single‑file templates.
- Testing without a provider –
MockLanguageModelV3lets you unit‑test agents offline. - Observability & budgets – docs include hooks for logging, usage tracking, and budget enforcement.
Typical use cases
- Customer‑service bots that must obey regulatory limits (e.g., refund caps, data‑privacy rules).
- Workflow automation where each step is a verified state transition (approval flows, ticket triage).
- Tool‑calling agents that need strict validation of tool arguments before execution.
- Self‑improving agents that can suggest policy updates, which are automatically vetted before deployment.
- Embedding in any stack – from serverless functions to long‑running durable objects, because the control flow lives entirely in the state machine.
Where to learn more
- Documentation: https://stately.ai/docs/packages/agent
- Examples:
examples/folder (Twenty Questions, Chameleon, Ticket triage, etc.) - Comparison to LangGraph:
docs/langgraph-comparison.md - Pattern library:
docs/patterns.md
TL;DR
Stately Agent lets you write LLM‑driven agents as formal state machines. The model only suggests events; the machine enforces business rules, making agents safe, testable, and portable across any JavaScript environment.
Related
- Project
- Dispatch
- Project
- Project
- Project