Skip to content

Latest commit

 

History

History
512 lines (400 loc) · 14.8 KB

File metadata and controls

512 lines (400 loc) · 14.8 KB

State Management

Agents provide built-in state management with automatic persistence and real-time synchronization across all connected clients.

Overview

Agent state is:

  • Persistent - Automatically saved to SQLite, survives restarts and hibernation
  • Synchronized - Changes broadcast to all connected WebSocket clients instantly
  • Bidirectional - Both server and clients can update state
  • Type-safe - Full TypeScript support with generics
import { Agent } from "agents";

type GameState = {
  players: string[];
  score: number;
  status: "waiting" | "playing" | "finished";
};

export class GameAgent extends Agent<Env, GameState> {
  // Default state for new agents
  initialState: GameState = {
    players: [],
    score: 0,
    status: "waiting"
  };

  // React to state changes
  onStateChanged(state: GameState, source: Connection | "server") {
    if (source !== "server" && state.players.length >= 2) {
      // Client added a player, start the game
      this.setState({ ...state, status: "playing" });
    }
  }

  addPlayer(name: string) {
    this.setState({
      ...this.state,
      players: [...this.state.players, name]
    });
  }
}

Defining Initial State

Use the initialState property to define default values for new agent instances:

type State = {
  messages: Message[];
  settings: UserSettings;
  lastActive: string | null;
};

export class ChatAgent extends Agent<Env, State> {
  initialState: State = {
    messages: [],
    settings: { theme: "dark", notifications: true },
    lastActive: null
  };
}

Type Safety

The second generic parameter to Agent<Env, State> defines your state type:

// State is fully typed
export class MyAgent extends Agent<Env, MyState> {
  initialState: MyState = { count: 0 };

  increment() {
    // TypeScript knows this.state is MyState
    this.setState({ count: this.state.count + 1 });
  }
}

When Initial State Applies

Initial state is applied lazily on first access, not on every wake:

  1. New agent - initialState is used and persisted
  2. Existing agent - Persisted state is loaded from SQLite
  3. No initialState defined - this.state is undefined
async onStart() {
  // Safe to access - returns initialState if new, or persisted state
  console.log("Current count:", this.state.count);
}

Reading State

Access the current state via the this.state getter:

async onRequest(request: Request) {
  // Read current state
  const { players, status } = this.state;

  if (status === "waiting" && players.length < 2) {
    return new Response("Waiting for players...");
  }

  return new Response(JSON.stringify(this.state));
}

Undefined State

If you don't define initialState, this.state returns undefined:

export class MinimalAgent extends Agent<Env> {
  // No initialState defined

  async onConnect(connection: Connection) {
    if (!this.state) {
      // First time - initialize state
      this.setState({ initialized: true });
    }
  }
}

Updating State

Use setState() to update state. This:

  1. Saves to SQLite (persistent)
  2. Broadcasts to all connected clients
  3. Triggers onStateChanged() (after broadcast; best-effort)
// Replace entire state
this.setState({
  players: ["Alice", "Bob"],
  score: 0,
  status: "playing"
});

// Update specific fields (spread existing state)
this.setState({
  ...this.state,
  score: this.state.score + 10
});

State Must Be Serializable

State is stored as JSON, so it must be serializable:

// Good - plain objects, arrays, primitives
this.setState({
  items: ["a", "b", "c"],
  count: 42,
  active: true,
  metadata: { key: "value" }
});

// Bad - functions, classes, circular references
this.setState({
  callback: () => {}, // Functions don't serialize
  date: new Date(), // Becomes string, loses methods
  self: this // Circular reference
});

// For dates, use ISO strings
this.setState({
  createdAt: new Date().toISOString()
});

Responding to State Changes

Override onStateChanged() to react when state changes (notifications/side-effects):

onStateChanged(state: GameState, source: Connection | "server") {
  console.log("State updated:", state);
  console.log("Updated by:", source === "server" ? "server" : source.id);
}

Validating State Updates

If you want to validate or reject state updates, override validateStateChange():

  • Runs before persistence and broadcast
  • Must be synchronous
  • Throwing aborts the update
validateStateChange(nextState: GameState, source: Connection | "server") {
  // Example: reject negative scores
  if (nextState.score < 0) {
    throw new Error("score cannot be negative");
  }
}

onStateChanged() is not intended for validation; it is a notification hook and should not block broadcasts.

Migration note: onStateChanged replaces the deprecated onStateUpdate (server-side hook). If you're using onStateUpdate on your agent class, rename it to onStateChanged — the signature and behavior are identical. A console warning will fire once per class until you rename it.

The source Parameter

The source tells you who triggered the update:

Value Meaning
"server" Agent called setState()
Connection A client pushed state via WebSocket

This is useful for:

  • Avoiding infinite loops (don't react to your own updates)
  • Validating client input
  • Triggering side effects only on client actions
onStateChanged(state: State, source: Connection | "server") {
  // Ignore server-initiated updates
  if (source === "server") return;

  // A client updated state - validate and process
  const connection = source;
  console.log(`Client ${connection.id} updated state`);

  // Maybe trigger something based on the change
  if (state.status === "submitted") {
    this.processSubmission(state);
  }
}

Common Pattern: Client-Driven Actions

onStateChanged(state: State, source: Connection | "server") {
  if (source === "server") return;

  // Client added a message
  const lastMessage = state.messages[state.messages.length - 1];
  if (lastMessage && !lastMessage.processed) {
    // Process and update
    this.setState({
      ...state,
      messages: state.messages.map(m =>
        m.id === lastMessage.id ? { ...m, processed: true } : m
      )
    });
  }
}

Client-Side State Sync

State synchronizes automatically with connected clients. Both useAgent and AgentClient expose a state property that tracks the current agent state. See Client SDK for full details.

React (useAgent)

import { useAgent } from "agents/react";

function GameUI() {
  const agent = useAgent({
    agent: "game-agent",
    name: "room-123"
  });

  // Read state directly — reactive, triggers re-render on change
  // Push state to agent with spread for partial updates
  const addPlayer = (name: string) => {
    agent.setState({
      ...agent.state,
      players: [...(agent.state?.players ?? []), name]
    });
  };

  return <div>Players: {agent.state?.players.join(", ")}</div>;
}

Vanilla JS (AgentClient)

import { AgentClient } from "agents/client";

const client = new AgentClient({
  agent: "game-agent",
  name: "room-123",
  host: "your-worker.workers.dev"
});

await client.ready;

// Read state directly
console.log("Score:", client.state?.score);

// Push state update with spread for partial updates
client.setState({ ...client.state, score: 100 });

State Flow

┌─────────────────────────────────────────────────────────────┐
│                         Agent                               │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                    this.state                       │    │
│  │              (persisted in SQLite)                  │    │
│  └─────────────────────────────────────────────────────┘    │
│           ▲                              │                  │
│           │ setState()                   │ broadcast        │
│           │                              ▼                  │
└───────────┼──────────────────────────────┼──────────────────┘
            │                              │
            │                              │ WebSocket
            │                              │
┌───────────┴──────────────────────────────┴───────────────────┐
│                        Clients                               │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐               │
│   │ Client 1 │    │ Client 2 │    │ Client 3 │               │
│   │  state   │    │  state   │    │  state   │               │
│   └──────────┘    └──────────┘    └──────────┘               │
│                                                              │
│   Any client can call setState() to push updates             │
└──────────────────────────────────────────────────────────────┘

State from Workflows

When using Workflows, you can update agent state from workflow steps:

// In your workflow
async run(event: AgentWorkflowEvent<Params>, step: AgentWorkflowStep) {
  // Replace entire state
  await step.updateAgentState({ status: "processing", progress: 0 });

  // Merge partial updates (preserves other fields)
  await step.mergeAgentState({ progress: 50 });

  // Reset to initialState
  await step.resetAgentState();

  return result;
}

These are durable operations - they persist even if the workflow retries.

Patterns & Best Practices

Keep State Small

State is broadcast to all clients on every change. For large data:

// Bad - storing large arrays in state
initialState = {
  allMessages: []  // Could grow to thousands of items
};

// Good - store in SQL, keep state light
initialState = {
  messageCount: 0,
  lastMessageId: null
};

// Query SQL for full data
async getMessages(limit = 50) {
  return this.sql`SELECT * FROM messages ORDER BY created_at DESC LIMIT ${limit}`;
}

Optimistic Updates

For responsive UIs, update client state immediately:

// Client-side
function sendMessage(text: string) {
  const optimisticMessage = {
    id: crypto.randomUUID(),
    text,
    pending: true
  };

  // Update immediately — agent.state updates optimistically
  agent.setState({
    ...agent.state,
    messages: [...(agent.state?.messages ?? []), optimisticMessage]
  });

  // Server will confirm/update
}

// Server-side
onStateChanged(state: State, source: Connection | "server") {
  if (source === "server") return;

  const pendingMessages = state.messages.filter(m => m.pending);
  for (const msg of pendingMessages) {
    // Validate and confirm
    this.setState({
      ...state,
      messages: state.messages.map(m =>
        m.id === msg.id ? { ...m, pending: false, timestamp: Date.now() } : m
      )
    });
  }
}

State vs SQL

Use State For Use SQL For
UI state (loading, selected items) Historical data
Real-time counters Large collections
Active session data Relationships
Configuration Queryable data
export class ChatAgent extends Agent<Env, State> {
  // State: current UI state
  initialState = {
    typing: [],
    unreadCount: 0,
    activeUsers: []
  };

  // SQL: message history
  async getMessages(limit = 100) {
    return this.sql`
      SELECT * FROM messages
      ORDER BY created_at DESC
      LIMIT ${limit}
    `;
  }

  async saveMessage(message: Message) {
    this.sql`
      INSERT INTO messages (id, text, user_id, created_at)
      VALUES (${message.id}, ${message.text}, ${message.userId}, ${Date.now()})
    `;
    // Update state for real-time UI
    this.setState({
      ...this.state,
      unreadCount: this.state.unreadCount + 1
    });
  }
}

Avoid Infinite Loops

Be careful not to trigger state updates in response to your own updates:

// Bad - infinite loop
onStateChanged(state: State) {
  this.setState({ ...state, lastUpdated: Date.now() });
}

// Good - check source
onStateChanged(state: State, source: Connection | "server") {
  if (source === "server") return;  // Don't react to own updates
  this.setState({ ...state, lastUpdated: Date.now() });
}

API Reference

Properties

Property Type Description
state State Current state (getter)
initialState State Default state for new agents

Methods

Method Signature Description
setState (state: State) => void Update state, persist, and broadcast
onStateChanged (state: State, source: Connection | "server") => void Called after state is persisted and broadcast

Workflow Step Methods

Method Description
step.updateAgentState(state) Replace agent state from workflow
step.mergeAgentState(partial) Merge partial state from workflow
step.resetAgentState() Reset to initialState from workflow

Next Steps