agents 0.16.2 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-CNyE1iz_.d.ts} +671 -138
- package/dist/agent-tool-types.d.ts +34 -18
- package/dist/agent-tool-types.js +20 -1
- package/dist/agent-tool-types.js.map +1 -0
- package/dist/agent-tools-BXlsuX0d.js +304 -0
- package/dist/agent-tools-BXlsuX0d.js.map +1 -0
- package/dist/agent-tools-CSnyGvJ2.d.ts +119 -0
- package/dist/agent-tools.d.ts +24 -18
- package/dist/agent-tools.js.map +1 -1
- package/dist/ai-chat-agent.d.ts +1 -1
- package/dist/ai-chat-agent.js +1 -2
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-chat-v5-migration.d.ts +1 -1
- package/dist/ai-chat-v5-migration.js +1 -2
- package/dist/ai-chat-v5-migration.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +1 -2
- package/dist/ai-react.js.map +1 -1
- package/dist/ai-types.d.ts +1 -7
- package/dist/ai-types.js +2 -8
- package/dist/ai-types.js.map +1 -1
- package/dist/browser/ai.js +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/chat/index.d.ts +2033 -77
- package/dist/chat/index.js +1828 -245
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +602 -0
- package/dist/chat/react.js +1506 -0
- package/dist/chat/react.js.map +1 -0
- package/dist/chat-sdk/index.d.ts +4 -4
- package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
- package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
- package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
- package/dist/client-BZ-B3NhC.js.map +1 -0
- package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
- package/dist/client.d.ts +76 -57
- package/dist/client.js +33 -5
- package/dist/client.js.map +1 -1
- package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
- package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
- package/dist/email.js +1 -1
- package/dist/email.js.map +1 -1
- package/dist/{index-B7IbEeze.d.ts → index-BRnybD6X.d.ts} +197 -14
- package/dist/index.d.ts +95 -73
- package/dist/index.js +694 -25
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +18 -14
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/index.d.ts +30 -30
- package/dist/mcp/index.js +7 -7
- package/dist/mcp/index.js.map +1 -1
- package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
- package/dist/message-builder-BymO4N_D.js.map +1 -0
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +4 -2
- package/dist/observability/index.js.map +1 -1
- package/dist/react.d.ts +123 -110
- package/dist/react.js +44 -11
- package/dist/react.js.map +1 -1
- package/dist/{retries-CwlpAGet.d.ts → retries-CvHJwSuh.d.ts} +15 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +44 -1
- package/dist/retries.js.map +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/skills/compile.js +1 -1
- package/dist/skills/compile.js.map +1 -1
- package/dist/skills/index.js +5 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/sub-routing.d.ts +6 -6
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/vite.js +5 -5
- package/dist/vite.js.map +1 -1
- package/dist/wire-types-nflOzNuU.js +240 -0
- package/dist/wire-types-nflOzNuU.js.map +1 -0
- package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
- package/dist/workflow-types.d.ts +25 -21
- package/dist/workflow-types.js.map +1 -1
- package/dist/workflows.d.ts +22 -21
- package/dist/workflows.js +31 -7
- package/dist/workflows.js.map +1 -1
- package/docs/adding-to-existing-project.md +450 -0
- package/docs/agent-class.md +503 -0
- package/docs/agent-tools.md +552 -0
- package/docs/browse-the-web.md +430 -0
- package/docs/callable-methods.md +627 -0
- package/docs/chat-agents.md +1696 -0
- package/docs/chat-sdk.md +181 -0
- package/docs/client-sdk.md +520 -0
- package/docs/client-tools-continuation.md +177 -0
- package/docs/codemode.md +440 -0
- package/docs/configuration.md +775 -0
- package/docs/cross-domain-authentication.md +171 -0
- package/docs/durable-execution.md +537 -0
- package/docs/email.md +663 -0
- package/docs/get-current-agent.md +204 -0
- package/docs/getting-started.md +305 -0
- package/docs/http-websockets.md +668 -0
- package/docs/human-in-the-loop.md +661 -0
- package/docs/index.md +151 -0
- package/docs/long-running-agents.md +730 -0
- package/docs/mcp-client.md +620 -0
- package/docs/mcp-servers.md +526 -0
- package/docs/mcp-transports.md +308 -0
- package/docs/migration-to-ai-sdk-v5.md +96 -0
- package/docs/migration-to-ai-sdk-v6.md +163 -0
- package/docs/observability.md +261 -0
- package/docs/push-notifications.md +367 -0
- package/docs/queue.md +329 -0
- package/docs/readonly-connections.md +278 -0
- package/docs/resumable-streaming.md +127 -0
- package/docs/retries.md +444 -0
- package/docs/routing.md +749 -0
- package/docs/scheduling.md +898 -0
- package/docs/securing-mcp-servers.md +359 -0
- package/docs/server-driven-messages.md +477 -0
- package/docs/sessions.md +1024 -0
- package/docs/state.md +512 -0
- package/docs/sub-agents.md +389 -0
- package/docs/webhooks.md +604 -0
- package/docs/workflows.md +877 -0
- package/package.json +41 -14
- package/dist/agent-tools-3zLG7MgA.js.map +0 -1
- package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
- package/dist/client-BXJ9n2f7.js.map +0 -1
package/docs/state.md
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# State Management
|
|
2
|
+
|
|
3
|
+
Agents provide built-in state management with automatic persistence and real-time synchronization across all connected clients.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Agent state is:
|
|
8
|
+
|
|
9
|
+
- **Persistent** - Automatically saved to SQLite, survives restarts and hibernation
|
|
10
|
+
- **Synchronized** - Changes broadcast to all connected WebSocket clients instantly
|
|
11
|
+
- **Bidirectional** - Both server and clients can update state
|
|
12
|
+
- **Type-safe** - Full TypeScript support with generics
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { Agent } from "agents";
|
|
16
|
+
|
|
17
|
+
type GameState = {
|
|
18
|
+
players: string[];
|
|
19
|
+
score: number;
|
|
20
|
+
status: "waiting" | "playing" | "finished";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class GameAgent extends Agent<Env, GameState> {
|
|
24
|
+
// Default state for new agents
|
|
25
|
+
initialState: GameState = {
|
|
26
|
+
players: [],
|
|
27
|
+
score: 0,
|
|
28
|
+
status: "waiting"
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// React to state changes
|
|
32
|
+
onStateChanged(state: GameState, source: Connection | "server") {
|
|
33
|
+
if (source !== "server" && state.players.length >= 2) {
|
|
34
|
+
// Client added a player, start the game
|
|
35
|
+
this.setState({ ...state, status: "playing" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
addPlayer(name: string) {
|
|
40
|
+
this.setState({
|
|
41
|
+
...this.state,
|
|
42
|
+
players: [...this.state.players, name]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Defining Initial State
|
|
49
|
+
|
|
50
|
+
Use the `initialState` property to define default values for new agent instances:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
type State = {
|
|
54
|
+
messages: Message[];
|
|
55
|
+
settings: UserSettings;
|
|
56
|
+
lastActive: string | null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export class ChatAgent extends Agent<Env, State> {
|
|
60
|
+
initialState: State = {
|
|
61
|
+
messages: [],
|
|
62
|
+
settings: { theme: "dark", notifications: true },
|
|
63
|
+
lastActive: null
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Type Safety
|
|
69
|
+
|
|
70
|
+
The second generic parameter to `Agent<Env, State>` defines your state type:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// State is fully typed
|
|
74
|
+
export class MyAgent extends Agent<Env, MyState> {
|
|
75
|
+
initialState: MyState = { count: 0 };
|
|
76
|
+
|
|
77
|
+
increment() {
|
|
78
|
+
// TypeScript knows this.state is MyState
|
|
79
|
+
this.setState({ count: this.state.count + 1 });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### When Initial State Applies
|
|
85
|
+
|
|
86
|
+
Initial state is applied lazily on first access, not on every wake:
|
|
87
|
+
|
|
88
|
+
1. **New agent** - `initialState` is used and persisted
|
|
89
|
+
2. **Existing agent** - Persisted state is loaded from SQLite
|
|
90
|
+
3. **No `initialState` defined** - `this.state` is `undefined`
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
async onStart() {
|
|
94
|
+
// Safe to access - returns initialState if new, or persisted state
|
|
95
|
+
console.log("Current count:", this.state.count);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Reading State
|
|
100
|
+
|
|
101
|
+
Access the current state via the `this.state` getter:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
async onRequest(request: Request) {
|
|
105
|
+
// Read current state
|
|
106
|
+
const { players, status } = this.state;
|
|
107
|
+
|
|
108
|
+
if (status === "waiting" && players.length < 2) {
|
|
109
|
+
return new Response("Waiting for players...");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new Response(JSON.stringify(this.state));
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Undefined State
|
|
117
|
+
|
|
118
|
+
If you don't define `initialState`, `this.state` returns `undefined`:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
export class MinimalAgent extends Agent<Env> {
|
|
122
|
+
// No initialState defined
|
|
123
|
+
|
|
124
|
+
async onConnect(connection: Connection) {
|
|
125
|
+
if (!this.state) {
|
|
126
|
+
// First time - initialize state
|
|
127
|
+
this.setState({ initialized: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Updating State
|
|
134
|
+
|
|
135
|
+
Use `setState()` to update state. This:
|
|
136
|
+
|
|
137
|
+
1. Saves to SQLite (persistent)
|
|
138
|
+
2. Broadcasts to all connected clients
|
|
139
|
+
3. Triggers `onStateChanged()` (after broadcast; best-effort)
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Replace entire state
|
|
143
|
+
this.setState({
|
|
144
|
+
players: ["Alice", "Bob"],
|
|
145
|
+
score: 0,
|
|
146
|
+
status: "playing"
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Update specific fields (spread existing state)
|
|
150
|
+
this.setState({
|
|
151
|
+
...this.state,
|
|
152
|
+
score: this.state.score + 10
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### State Must Be Serializable
|
|
157
|
+
|
|
158
|
+
State is stored as JSON, so it must be serializable:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// Good - plain objects, arrays, primitives
|
|
162
|
+
this.setState({
|
|
163
|
+
items: ["a", "b", "c"],
|
|
164
|
+
count: 42,
|
|
165
|
+
active: true,
|
|
166
|
+
metadata: { key: "value" }
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Bad - functions, classes, circular references
|
|
170
|
+
this.setState({
|
|
171
|
+
callback: () => {}, // Functions don't serialize
|
|
172
|
+
date: new Date(), // Becomes string, loses methods
|
|
173
|
+
self: this // Circular reference
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// For dates, use ISO strings
|
|
177
|
+
this.setState({
|
|
178
|
+
createdAt: new Date().toISOString()
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Responding to State Changes
|
|
183
|
+
|
|
184
|
+
Override `onStateChanged()` to react when state changes (notifications/side-effects):
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
onStateChanged(state: GameState, source: Connection | "server") {
|
|
188
|
+
console.log("State updated:", state);
|
|
189
|
+
console.log("Updated by:", source === "server" ? "server" : source.id);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Validating State Updates
|
|
194
|
+
|
|
195
|
+
If you want to validate or reject state updates, override `validateStateChange()`:
|
|
196
|
+
|
|
197
|
+
- **Runs before persistence and broadcast**
|
|
198
|
+
- **Must be synchronous**
|
|
199
|
+
- **Throwing aborts the update**
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
validateStateChange(nextState: GameState, source: Connection | "server") {
|
|
203
|
+
// Example: reject negative scores
|
|
204
|
+
if (nextState.score < 0) {
|
|
205
|
+
throw new Error("score cannot be negative");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
> `onStateChanged()` is not intended for validation; it is a notification hook and should not block broadcasts.
|
|
211
|
+
>
|
|
212
|
+
> **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.
|
|
213
|
+
|
|
214
|
+
### The `source` Parameter
|
|
215
|
+
|
|
216
|
+
The `source` tells you who triggered the update:
|
|
217
|
+
|
|
218
|
+
| Value | Meaning |
|
|
219
|
+
| ------------ | ----------------------------------- |
|
|
220
|
+
| `"server"` | Agent called `setState()` |
|
|
221
|
+
| `Connection` | A client pushed state via WebSocket |
|
|
222
|
+
|
|
223
|
+
This is useful for:
|
|
224
|
+
|
|
225
|
+
- Avoiding infinite loops (don't react to your own updates)
|
|
226
|
+
- Validating client input
|
|
227
|
+
- Triggering side effects only on client actions
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
onStateChanged(state: State, source: Connection | "server") {
|
|
231
|
+
// Ignore server-initiated updates
|
|
232
|
+
if (source === "server") return;
|
|
233
|
+
|
|
234
|
+
// A client updated state - validate and process
|
|
235
|
+
const connection = source;
|
|
236
|
+
console.log(`Client ${connection.id} updated state`);
|
|
237
|
+
|
|
238
|
+
// Maybe trigger something based on the change
|
|
239
|
+
if (state.status === "submitted") {
|
|
240
|
+
this.processSubmission(state);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Common Pattern: Client-Driven Actions
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
onStateChanged(state: State, source: Connection | "server") {
|
|
249
|
+
if (source === "server") return;
|
|
250
|
+
|
|
251
|
+
// Client added a message
|
|
252
|
+
const lastMessage = state.messages[state.messages.length - 1];
|
|
253
|
+
if (lastMessage && !lastMessage.processed) {
|
|
254
|
+
// Process and update
|
|
255
|
+
this.setState({
|
|
256
|
+
...state,
|
|
257
|
+
messages: state.messages.map(m =>
|
|
258
|
+
m.id === lastMessage.id ? { ...m, processed: true } : m
|
|
259
|
+
)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Client-Side State Sync
|
|
266
|
+
|
|
267
|
+
State synchronizes automatically with connected clients. Both `useAgent` and `AgentClient` expose a `state` property that tracks the current agent state. See [Client SDK](./client-sdk.md) for full details.
|
|
268
|
+
|
|
269
|
+
### React (useAgent)
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
import { useAgent } from "agents/react";
|
|
273
|
+
|
|
274
|
+
function GameUI() {
|
|
275
|
+
const agent = useAgent({
|
|
276
|
+
agent: "game-agent",
|
|
277
|
+
name: "room-123"
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Read state directly — reactive, triggers re-render on change
|
|
281
|
+
// Push state to agent with spread for partial updates
|
|
282
|
+
const addPlayer = (name: string) => {
|
|
283
|
+
agent.setState({
|
|
284
|
+
...agent.state,
|
|
285
|
+
players: [...(agent.state?.players ?? []), name]
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return <div>Players: {agent.state?.players.join(", ")}</div>;
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Vanilla JS (AgentClient)
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { AgentClient } from "agents/client";
|
|
297
|
+
|
|
298
|
+
const client = new AgentClient({
|
|
299
|
+
agent: "game-agent",
|
|
300
|
+
name: "room-123",
|
|
301
|
+
host: "your-worker.workers.dev"
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await client.ready;
|
|
305
|
+
|
|
306
|
+
// Read state directly
|
|
307
|
+
console.log("Score:", client.state?.score);
|
|
308
|
+
|
|
309
|
+
// Push state update with spread for partial updates
|
|
310
|
+
client.setState({ ...client.state, score: 100 });
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### State Flow
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
317
|
+
│ Agent │
|
|
318
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
319
|
+
│ │ this.state │ │
|
|
320
|
+
│ │ (persisted in SQLite) │ │
|
|
321
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
322
|
+
│ ▲ │ │
|
|
323
|
+
│ │ setState() │ broadcast │
|
|
324
|
+
│ │ ▼ │
|
|
325
|
+
└───────────┼──────────────────────────────┼──────────────────┘
|
|
326
|
+
│ │
|
|
327
|
+
│ │ WebSocket
|
|
328
|
+
│ │
|
|
329
|
+
┌───────────┴──────────────────────────────┴───────────────────┐
|
|
330
|
+
│ Clients │
|
|
331
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
332
|
+
│ │ Client 1 │ │ Client 2 │ │ Client 3 │ │
|
|
333
|
+
│ │ state │ │ state │ │ state │ │
|
|
334
|
+
│ └──────────┘ └──────────┘ └──────────┘ │
|
|
335
|
+
│ │
|
|
336
|
+
│ Any client can call setState() to push updates │
|
|
337
|
+
└──────────────────────────────────────────────────────────────┘
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## State from Workflows
|
|
341
|
+
|
|
342
|
+
When using [Workflows](./workflows.md), you can update agent state from workflow steps:
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// In your workflow
|
|
346
|
+
async run(event: AgentWorkflowEvent<Params>, step: AgentWorkflowStep) {
|
|
347
|
+
// Replace entire state
|
|
348
|
+
await step.updateAgentState({ status: "processing", progress: 0 });
|
|
349
|
+
|
|
350
|
+
// Merge partial updates (preserves other fields)
|
|
351
|
+
await step.mergeAgentState({ progress: 50 });
|
|
352
|
+
|
|
353
|
+
// Reset to initialState
|
|
354
|
+
await step.resetAgentState();
|
|
355
|
+
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
These are durable operations - they persist even if the workflow retries.
|
|
361
|
+
|
|
362
|
+
## Patterns & Best Practices
|
|
363
|
+
|
|
364
|
+
### Keep State Small
|
|
365
|
+
|
|
366
|
+
State is broadcast to all clients on every change. For large data:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// Bad - storing large arrays in state
|
|
370
|
+
initialState = {
|
|
371
|
+
allMessages: [] // Could grow to thousands of items
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Good - store in SQL, keep state light
|
|
375
|
+
initialState = {
|
|
376
|
+
messageCount: 0,
|
|
377
|
+
lastMessageId: null
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// Query SQL for full data
|
|
381
|
+
async getMessages(limit = 50) {
|
|
382
|
+
return this.sql`SELECT * FROM messages ORDER BY created_at DESC LIMIT ${limit}`;
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Optimistic Updates
|
|
387
|
+
|
|
388
|
+
For responsive UIs, update client state immediately:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Client-side
|
|
392
|
+
function sendMessage(text: string) {
|
|
393
|
+
const optimisticMessage = {
|
|
394
|
+
id: crypto.randomUUID(),
|
|
395
|
+
text,
|
|
396
|
+
pending: true
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Update immediately — agent.state updates optimistically
|
|
400
|
+
agent.setState({
|
|
401
|
+
...agent.state,
|
|
402
|
+
messages: [...(agent.state?.messages ?? []), optimisticMessage]
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Server will confirm/update
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Server-side
|
|
409
|
+
onStateChanged(state: State, source: Connection | "server") {
|
|
410
|
+
if (source === "server") return;
|
|
411
|
+
|
|
412
|
+
const pendingMessages = state.messages.filter(m => m.pending);
|
|
413
|
+
for (const msg of pendingMessages) {
|
|
414
|
+
// Validate and confirm
|
|
415
|
+
this.setState({
|
|
416
|
+
...state,
|
|
417
|
+
messages: state.messages.map(m =>
|
|
418
|
+
m.id === msg.id ? { ...m, pending: false, timestamp: Date.now() } : m
|
|
419
|
+
)
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### State vs SQL
|
|
426
|
+
|
|
427
|
+
| Use State For | Use SQL For |
|
|
428
|
+
| ---------------------------------- | ----------------- |
|
|
429
|
+
| UI state (loading, selected items) | Historical data |
|
|
430
|
+
| Real-time counters | Large collections |
|
|
431
|
+
| Active session data | Relationships |
|
|
432
|
+
| Configuration | Queryable data |
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
export class ChatAgent extends Agent<Env, State> {
|
|
436
|
+
// State: current UI state
|
|
437
|
+
initialState = {
|
|
438
|
+
typing: [],
|
|
439
|
+
unreadCount: 0,
|
|
440
|
+
activeUsers: []
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// SQL: message history
|
|
444
|
+
async getMessages(limit = 100) {
|
|
445
|
+
return this.sql`
|
|
446
|
+
SELECT * FROM messages
|
|
447
|
+
ORDER BY created_at DESC
|
|
448
|
+
LIMIT ${limit}
|
|
449
|
+
`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async saveMessage(message: Message) {
|
|
453
|
+
this.sql`
|
|
454
|
+
INSERT INTO messages (id, text, user_id, created_at)
|
|
455
|
+
VALUES (${message.id}, ${message.text}, ${message.userId}, ${Date.now()})
|
|
456
|
+
`;
|
|
457
|
+
// Update state for real-time UI
|
|
458
|
+
this.setState({
|
|
459
|
+
...this.state,
|
|
460
|
+
unreadCount: this.state.unreadCount + 1
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Avoid Infinite Loops
|
|
467
|
+
|
|
468
|
+
Be careful not to trigger state updates in response to your own updates:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// Bad - infinite loop
|
|
472
|
+
onStateChanged(state: State) {
|
|
473
|
+
this.setState({ ...state, lastUpdated: Date.now() });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Good - check source
|
|
477
|
+
onStateChanged(state: State, source: Connection | "server") {
|
|
478
|
+
if (source === "server") return; // Don't react to own updates
|
|
479
|
+
this.setState({ ...state, lastUpdated: Date.now() });
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
## API Reference
|
|
484
|
+
|
|
485
|
+
### Properties
|
|
486
|
+
|
|
487
|
+
| Property | Type | Description |
|
|
488
|
+
| -------------- | ------- | ---------------------------- |
|
|
489
|
+
| `state` | `State` | Current state (getter) |
|
|
490
|
+
| `initialState` | `State` | Default state for new agents |
|
|
491
|
+
|
|
492
|
+
### Methods
|
|
493
|
+
|
|
494
|
+
| Method | Signature | Description |
|
|
495
|
+
| ---------------- | -------------------------------------------------------- | --------------------------------------------- |
|
|
496
|
+
| `setState` | `(state: State) => void` | Update state, persist, and broadcast |
|
|
497
|
+
| `onStateChanged` | `(state: State, source: Connection \| "server") => void` | Called after state is persisted and broadcast |
|
|
498
|
+
|
|
499
|
+
### Workflow Step Methods
|
|
500
|
+
|
|
501
|
+
| Method | Description |
|
|
502
|
+
| ------------------------------- | ------------------------------------- |
|
|
503
|
+
| `step.updateAgentState(state)` | Replace agent state from workflow |
|
|
504
|
+
| `step.mergeAgentState(partial)` | Merge partial state from workflow |
|
|
505
|
+
| `step.resetAgentState()` | Reset to `initialState` from workflow |
|
|
506
|
+
|
|
507
|
+
## Next Steps
|
|
508
|
+
|
|
509
|
+
- [Readonly Connections](./readonly-connections.md) - Restrict which connections can update state
|
|
510
|
+
- [Client SDK](./client-sdk.md) - Full client-side state sync documentation
|
|
511
|
+
- [Workflows](./workflows.md) - Durable state updates from workflows
|
|
512
|
+
- TODO: SQL API - When to use SQL instead of state
|