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
  1. Machine defines what the agent can do.
  2. runAgent invokes the machine and hands control to executors.
  3. Executors call the chosen LLM (via AI SDK, OpenAI SDK, etc.).
  4. The LLM returns an event; the machine validates it against its guards.
  5. If valid, the machine transitions; otherwise the model is asked again.

Key features (as listed in the README)

  • Typed contracts – zod schemas 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 verifyAgentConfig gates 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 – MockLanguageModelV3 lets 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