Stately Agent

repository·next·Indexed 18 days ago

https://github.com/statelyai/agent

A framework for building deterministic, inspectable, and resumable AI agents using XState machines. It separates agent control flow from model execution to ensure LLMs only trigger valid, pre-defined transitions. Supports patterns such as Human-in-the-Loop, Plan-and-Execute, RAG, and Time Travel, with integration options for Express, Hono, Next.js, React, and the Vercel AI SDK.

Tokens
90.2K
Snippets
189
Records
271
Agent score
63%

What's inside @statelyai/agent

  1. What is an agent machine?

    next

    An agent machine is a typed XState state machine that serves as a blueprint for an agent's behavior. It defines:

    • Which states exist.
    • Which transitions are legal.
    • Which model calls (requests) occur.
    • Which events the model may choose to emit.

    Crucially, the machine itself is a pure logic definition; it does not talk to a model directly. Instead, it describes the intent, and a host (the execution environment) performs the actual model calls and executes the machine.

  2. How the event log model works

    next

    In @statelyai/agent, a machine's durable state is defined by its event log rather than a snapshot. The log is an ordered array of external inputs (effect completions, user events, timer firings). Because transitions are pure, replaying this array reconstructs the exact state, including pending effects.

    Key Rules for Machine Authors:

    • External inputs only: Never log internal or raised events; replay re-derives them. Logging them causes double-application.
    • Purity is mandatory: Transitions and effect inputs (prompt builders, spawn inputs) must be pure functions of state and event. Do not use Date.now() or Math.random() directly; instead, inject time and randomness as events or inputs.
  3. Configure final states and machine output

    next

    A final state ends the machine or a region. Its output is typed against the machine's output schema.

    If the root machine declares no output but exactly one final state does, createMachine promotes that output to the root, making it available via snapshot.output.

    Best Practice: Always read context in a final output function, never the entering event. Because a final output function can be evaluated multiple times with different events, the event is unreliable. Capture necessary data into context during the transition to the final state instead.

    done: {
      type: 'final',
      output: ({ context }) => ({ answer: context.answer ?? '' }),
    }
  4. Emit and handle typed domain events

    next

    You can emit custom domain events from within your machine using enq.emit(...). To ensure type safety, declare the emitted event schemas in setupAgent.

    Emitted events are 'fire-and-forget' observations; they do not affect the machine's control flow or state transitions. They are ideal for triggering UI updates, SSE streams, or logging.

    Workflow:

    1. Define the event schema in the emitted property of setupAgent using Zod.
    2. Emit the event inside the machine (e.g., in an onDone handler) using enq.emit({ type: 'EVENT_NAME', ... }).
    3. Handle the event in the runAgent configuration using the on property.
    const agentSetup = setupAgent({
      context: z.object({ /* ... */ }),
      emitted: {
        EVALUATED: z.object({ qualityScore: z.number(), iteration: z.number() }),
      },
      // ...
    });
    
    // In the machine, from any transition or entry function:
    onDone: ({ context, output }, enq) => {
      enq.emit({ type: 'EVALUATED', qualityScore: output.score, iteration: context.iteration });
      return { target: 'checking', context: { evaluation: output } };
    },
  5. Control and safety patterns

    next

    Implement reliability, safety, and human oversight using these patterns:

    PatternPurposeImplementation Detail
    Human in the loopPause for approval, persist, and resume in another process.An idle state acts as a durable pause; the snapshot is plain JSON that can be stored anywhere.
    GuardrailsGate input and output through explicit validation states.Uses gate states with guards rather than prompt engineering; illegal paths are unreachable.
    Context compactionBound the context window by summarizing stale turns.A compacting state folds old history into a running summary once a threshold is reached.
    Customer supportConditional escalation to a human on sensitive actions.Sensitive actions gate on an interrupt state, preventing the model from acting past the guard.

    Durable Threads and Long Pauses

    For workflows requiring long-running processes or persistence across different environments:

    • Long-running onboarding: Use a multi-day coordinator with durable typed state and dormancy gates.
    • File-backed snapshot store: Use a file-based store to maintain durable threads across different processes.

    For more on pausing and resuming, see Human in the loop.

  6. Constraints and limits of generated machines

    next

    When using an LLM to generate agent machines, be aware of the following technical constraints:

    • Host-Resolved Guards and Actions: A configuration cannot contain actual functions. It can only contain string names (e.g., guard: "isReady"). The actual implementation must be provided in the guards object passed to setupAgent.fromConfig(...). You should list the available names in your prompt to guide the model.
    • Schema vs. Semantic Validity: Ajv validates the shape of the JSON, but not the meaning. A config might be schema-valid but logically broken (e.g., referencing a non-existent request). Use lintAgentMachine and simulateAgent to catch these semantic errors.
    • Simulation Coverage: simulateAgent follows a specific script and only covers one path. If you need to verify that a generated branch structure is sound, use explorePaths.
    • Unchecked String References: Model references and tool names are treated as unchecked strings in the config. They are resolved at runtime by the host. When using generated machines, ensure you pass createAiSdkExecutors({ resolveModel }) to handle these string-based model references.
  7. Evaluation and Observability patterns

    next

    To ensure agent quality and monitor performance, use the following patterns:

    Evaluation

    • Simulated User Evaluation: Use a pattern where a target chatbot and a simulated user alternate turns under a turn bound, followed by an independent judge scoring the transcript.
    • Retrofitting: When migrating from a hand-rolled while loop to a machine, use a stepwise refactor. Use simulateAgent tests to pin behavior before and after each step to ensure parity.

    Observability

    • OpenTelemetry (OTel): Use createOtelTraceHandler from @statelyai/agent/otel to export real spans over OTLP to platforms like LangSmith. If no key is provided, it exports to memory and prints the span tree.
  8. Understand the AgentMessage model

    next

    An AgentMessage is a parts-based, discriminated union representing a single conversation turn. It is designed to mirror the Vercel AI SDK's ModelMessage structure without requiring a dependency on the ai package.

    Messages are categorized by role, which determines the allowed structure of the content field:

    • system: content must be a string.
    • user: content can be a string, or an array of TextPart, ImagePart, or FilePart.
    • assistant: content can be a string, or an array of TextPart, FilePart, ToolCallPart, or ToolResultPart.
    • tool: content must be an array of ToolResultPart.

    Note on Serialization: ImagePart and FilePart can contain binary data (Uint8Array, ArrayBuffer) or URL instances. These are not JSON-serializable. If you plan to persist messages (e.g., to a database), you must convert binary data to base64 strings and URL instances to strings manually.

    type AgentMessage = SystemMessage | UserMessage | AssistantMessage | ToolMessage;
  9. How conditional edges and guards work compared to LangGraph

    next

    In Stately Agent, conditional logic is handled via guarded transitions rather than router functions.

    Instead of a node returning a string that a router function uses to pick the next node, the model selects from a set of allowedEvents. The transition is then governed by a guard (a function attached to an event). If the guard returns undefined, that transition is considered illegal for the current state.

    **Key differences:

    • Model interaction: The model picks a named event. If the guard rejects it, the attempt is recorded as rejected-by-guard and the model is prompted again with that feedback.
    • Safety: Constraints are enforced by the machine's guards, meaning they hold regardless of what the prompt instructs the model to do.
    • Typo prevention: The model picks from defined event names rather than generating arbitrary routing strings.
    // Example of a guarded transition in a machine definition
    grading: {
      invoke: {
        src: "agent.decide",
        input: ({ context }) => ({
          model: "grader",
          system: "GENERATE if the documents answer the question, else REWRITE.",
          prompt: `Question:\n${context.question}\n\nDocuments:\n${context.docs}`,
          allowedEvents: ["GENERATE", "REWRITE"],
        }),
      },
      on: {
        GENERATE: { target: "generating" },
        // The guard: returns undefined if the condition isn't met, making the transition illegal
        REWRITE: ({ context }) =>
          context.rewrites < 2
            ? { target: "rewriting", context: { rewrites: context.rewrites + 1 } }
            : undefined,
      },
    }
  10. Use expressions for data resolution in configs

    next

    Configs use string expressions in the format "{{ path.to.value }}" to resolve data at runtime. The resolver walks the path against input, context, and event.

    Rules:

    • No eval() is used; it is a pure path resolver.
    • Exemptions: The following fields are passed through verbatim and are NOT template-evaluated:
      • state, invoke, and transition meta fields.
      • A request's toolChoice field.
  11. Run agents as embedded tools

    next
    A complete agent machine can be embedded inside a single tool call of a host harness. This is achieved by using a bridge that handles JSON-safe snapshot handles and reads typed interaction metadata to start or resume the tool.
  12. Multi-agent patterns

    next

    Stately Agent supports several architectural patterns for coordinating multiple agents or specialized workers. These patterns allow you to manage complexity by delegating tasks to typed child actors or specialized machines.

    Available Patterns

    PatternPurposeImplementation Detail
    SupervisorA router dispatches to one of several specialist workers.Uses structured output to hand off to a typed worker; the graph acts as the organizational chart.
    Swarm handoffSpecialists hand the conversation off to each other across turns.Handoffs are transitions between typed child actors, persisted across turns.
    Orchestrator-workerAn orchestrator fans work out to workers and gathers results.Uses Promise.all over host actors for fan-out and join coordination.
    Fan-out (map-reduce)Plan N subtasks at runtime, run them, and reduce the results.Provides dynamic parallelism from a planner followed by a deterministic reduce state.
    Hierarchical teamsA coordinator invokes child team machines.Each team is a nested machine with a typed boundary.
    Whole-org workflowComplex workflows involving parallel analysts, debate, and approval.A single composite workflow where reject-and-revise loops are handled via states rather than retries.
    Sub-agentsCompose agent machines as child actors or host tools.Each child maintains its own executor binding while parents remain typed against results.

    For detailed implementation of sub-agents and child actors, see the Multi-agent documentation.