agents 0.16.1 → 0.17.0
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-NofdbL9X.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +595 -137
- 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-_E8wxUIK.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 +1923 -76
- package/dist/chat/index.js +1784 -278
- 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-FUizKzj2.js → client-BZ-B3NhC.js} +19 -3
- 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/{compaction-helpers-DVcu5lPN.d.ts → compaction-helpers-wUz6M3us.d.ts} +20 -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/experimental/memory/session/index.d.ts +1 -1
- package/dist/experimental/memory/session/index.js +20 -2
- package/dist/experimental/memory/session/index.js.map +1 -1
- package/dist/experimental/memory/utils/index.d.ts +1 -1
- package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
- package/dist/index.d.ts +91 -71
- package/dist/index.js +562 -24
- 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/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 +1687 -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 +47 -15
- package/dist/agent-tools-3zLG7MgA.js.map +0 -1
- package/dist/agent-tools-DLquv-dp.d.ts +0 -14
- package/dist/client-FUizKzj2.js.map +0 -1
package/docs/queue.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Queue System
|
|
2
|
+
|
|
3
|
+
The Agents SDK provides a built-in queue system that allows you to schedule tasks for asynchronous execution. This is particularly useful for background processing, delayed operations, and managing workloads that don't need immediate execution.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The queue system is built into the base `Agent` class. Tasks are stored in a SQLite table and processed automatically in FIFO (First In, First Out) order.
|
|
8
|
+
|
|
9
|
+
## QueueItem Type
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
export type QueueItem<T = string> = {
|
|
13
|
+
id: string; // Unique identifier for the queued task
|
|
14
|
+
payload: T; // Data to pass to the callback function
|
|
15
|
+
callback: keyof Agent<unknown>; // Name of the method to call
|
|
16
|
+
created_at: number; // Timestamp when the task was created
|
|
17
|
+
retry?: RetryOptions; // Retry options (if configured)
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Core Methods
|
|
22
|
+
|
|
23
|
+
### queue()
|
|
24
|
+
|
|
25
|
+
Adds a task to the queue for future execution.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
async queue<T = unknown>(
|
|
29
|
+
callback: keyof this,
|
|
30
|
+
payload: T,
|
|
31
|
+
options?: { retry?: RetryOptions }
|
|
32
|
+
): Promise<string>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Parameters:**
|
|
36
|
+
|
|
37
|
+
- `callback`: The name of the method to call when processing the task
|
|
38
|
+
- `payload`: Data to pass to the callback method
|
|
39
|
+
- `options.retry`: Optional retry configuration. See [Retries](./retries.md) for details.
|
|
40
|
+
|
|
41
|
+
**Returns:** The unique ID of the queued task
|
|
42
|
+
|
|
43
|
+
**Example:**
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
class MyAgent extends Agent {
|
|
47
|
+
async processEmail(data: { email: string; subject: string }) {
|
|
48
|
+
// Process the email
|
|
49
|
+
console.log(`Processing email: ${data.subject}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async onMessage(message: string) {
|
|
53
|
+
// Queue an email processing task
|
|
54
|
+
const taskId = await this.queue("processEmail", {
|
|
55
|
+
email: "user@example.com",
|
|
56
|
+
subject: "Welcome!"
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
console.log(`Queued task with ID: ${taskId}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### dequeue()
|
|
65
|
+
|
|
66
|
+
Removes a specific task from the queue by ID.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
dequeue(id: string): void
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Parameters:**
|
|
73
|
+
|
|
74
|
+
- `id`: The ID of the task to remove
|
|
75
|
+
|
|
76
|
+
**Example:**
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Remove a specific task
|
|
80
|
+
agent.dequeue("abc123def");
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### dequeueAll()
|
|
84
|
+
|
|
85
|
+
Removes all tasks from the queue.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
dequeueAll(): void
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Example:**
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Clear the entire queue
|
|
95
|
+
agent.dequeueAll();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### dequeueAllByCallback()
|
|
99
|
+
|
|
100
|
+
Removes all tasks that match a specific callback method.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
dequeueAllByCallback(callback: string): void
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Parameters:**
|
|
107
|
+
|
|
108
|
+
- `callback`: Name of the callback method
|
|
109
|
+
|
|
110
|
+
**Example:**
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Remove all email processing tasks
|
|
114
|
+
agent.dequeueAllByCallback("processEmail");
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### getQueue()
|
|
118
|
+
|
|
119
|
+
Retrieves a specific queued task by ID.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
getQueue(id: string): QueueItem<string> | undefined
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Parameters:**
|
|
126
|
+
|
|
127
|
+
- `id`: The ID of the task to retrieve
|
|
128
|
+
|
|
129
|
+
**Returns:** The QueueItem with parsed payload or undefined if not found
|
|
130
|
+
|
|
131
|
+
**Note:** The payload is automatically parsed from JSON before being returned
|
|
132
|
+
|
|
133
|
+
**Example:**
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const task = agent.getQueue("abc123def");
|
|
137
|
+
if (task) {
|
|
138
|
+
console.log(`Task callback: ${task.callback}`);
|
|
139
|
+
console.log(`Task payload:`, task.payload);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### getQueues()
|
|
144
|
+
|
|
145
|
+
Retrieves all queued tasks that match a specific key-value pair in their payload.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
getQueues(key: string, value: string): QueueItem<string>[]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Parameters:**
|
|
152
|
+
|
|
153
|
+
- `key`: The key to filter by in the payload
|
|
154
|
+
- `value`: The value to match
|
|
155
|
+
|
|
156
|
+
**Returns:** Array of matching QueueItem objects
|
|
157
|
+
|
|
158
|
+
**Note:** This method fetches all queue items and filters them in memory by parsing each payload and checking if the specified key matches the value
|
|
159
|
+
|
|
160
|
+
**Example:**
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Find all tasks for a specific user
|
|
164
|
+
const userTasks = await agent.getQueues("userId", "12345");
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## How Queue Processing Works
|
|
168
|
+
|
|
169
|
+
1. **Validation**: When calling `queue()`, the method validates that the callback exists as a function on the agent
|
|
170
|
+
2. **Automatic Processing**: After queuing, the system automatically attempts to flush the queue
|
|
171
|
+
3. **FIFO Order**: Tasks are processed in the order they were created (`created_at` timestamp)
|
|
172
|
+
4. **Context Preservation**: Each queued task runs with the same agent context (connection, request, email)
|
|
173
|
+
5. **Automatic Retries**: If a callback fails, it is retried with exponential backoff (configurable per task)
|
|
174
|
+
6. **Automatic Dequeue**: Tasks are removed from the queue after successful execution or after all retry attempts are exhausted
|
|
175
|
+
7. **Error Handling**: If a callback method does not exist at execution time, an error is logged and the task is skipped
|
|
176
|
+
8. **Persistence**: Tasks are stored in the `cf_agents_queues` table and survive agent restarts
|
|
177
|
+
|
|
178
|
+
## Queue Callback Methods
|
|
179
|
+
|
|
180
|
+
When defining callback methods for queued tasks, they must follow this signature:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
async callbackMethod(payload: unknown, queueItem: QueueItem<string>): Promise<void>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Example:**
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
class MyAgent extends Agent {
|
|
190
|
+
async sendNotification(
|
|
191
|
+
payload: { userId: string; message: string },
|
|
192
|
+
queueItem: QueueItem<string>
|
|
193
|
+
) {
|
|
194
|
+
console.log(`Processing task ${queueItem.id}`);
|
|
195
|
+
console.log(
|
|
196
|
+
`Sending notification to user ${payload.userId}: ${payload.message}`
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Your notification logic here
|
|
200
|
+
await this.notificationService.send(payload.userId, payload.message);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async onUserSignup(userData: any) {
|
|
204
|
+
// Queue a welcome notification
|
|
205
|
+
await this.queue("sendNotification", {
|
|
206
|
+
userId: userData.id,
|
|
207
|
+
message: "Welcome to our platform!"
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Use Cases
|
|
214
|
+
|
|
215
|
+
### Background Processing
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
class DataProcessor extends Agent {
|
|
219
|
+
async processLargeDataset(data: { datasetId: string; userId: string }) {
|
|
220
|
+
const results = await this.heavyComputation(data.datasetId);
|
|
221
|
+
await this.notifyUser(data.userId, results);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async onDataUpload(uploadData: any) {
|
|
225
|
+
// Queue the processing instead of doing it synchronously
|
|
226
|
+
await this.queue("processLargeDataset", {
|
|
227
|
+
datasetId: uploadData.id,
|
|
228
|
+
userId: uploadData.userId
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return { message: "Data upload received, processing started" };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Delayed Operations
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
class ReminderAgent extends Agent {
|
|
240
|
+
async sendReminder(data: { userId: string; message: string }) {
|
|
241
|
+
await this.emailService.send(data.userId, data.message);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async scheduleReminder(userId: string, message: string, delayMs: number) {
|
|
245
|
+
// Note: For true delayed execution, combine with the scheduling system
|
|
246
|
+
// This example shows queueing for later processing
|
|
247
|
+
await this.queue("sendReminder", { userId, message });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Batch Operations
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
class BatchProcessor extends Agent {
|
|
256
|
+
async processBatch(data: { items: any[]; batchId: string }) {
|
|
257
|
+
for (const item of data.items) {
|
|
258
|
+
await this.processItem(item);
|
|
259
|
+
}
|
|
260
|
+
console.log(`Completed batch ${data.batchId}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async onLargeRequest(items: any[]) {
|
|
264
|
+
// Split large requests into smaller batches
|
|
265
|
+
const batchSize = 10;
|
|
266
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
267
|
+
const batch = items.slice(i, i + batchSize);
|
|
268
|
+
await this.queue("processBatch", {
|
|
269
|
+
items: batch,
|
|
270
|
+
batchId: `batch-${i / batchSize + 1}`
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Best Practices
|
|
278
|
+
|
|
279
|
+
1. **Keep Payloads Small**: Payloads are JSON-serialized and stored in the database
|
|
280
|
+
2. **Idempotent Operations**: Design callback methods to be safe to retry
|
|
281
|
+
3. **Error Handling**: Include proper error handling in callback methods
|
|
282
|
+
4. **Monitoring**: Use logging to track queue processing
|
|
283
|
+
5. **Cleanup**: Regularly clean up completed or failed tasks if needed
|
|
284
|
+
|
|
285
|
+
## Error Handling and Retries
|
|
286
|
+
|
|
287
|
+
Queued tasks are automatically retried on failure with exponential backoff. The default is 3 attempts. You can customize this per task:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Retry up to 5 times with custom backoff
|
|
291
|
+
await this.queue("reliableTask", payload, {
|
|
292
|
+
retry: { maxAttempts: 5, baseDelayMs: 500 }
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
If you need custom error handling in the callback:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
class RobustAgent extends Agent {
|
|
300
|
+
async reliableTask(payload: { data: string }, queueItem: QueueItem<string>) {
|
|
301
|
+
try {
|
|
302
|
+
await this.doSomethingRisky(payload);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error(`Task ${queueItem.id} failed:`, error);
|
|
305
|
+
// The retry system will catch this error and retry automatically
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
See [Retries](./retries.md) for full documentation on retry options and patterns.
|
|
313
|
+
|
|
314
|
+
## Integration with Other Features
|
|
315
|
+
|
|
316
|
+
The queue system works seamlessly with other Agent SDK features:
|
|
317
|
+
|
|
318
|
+
- **State Management**: Access agent state within queued callbacks
|
|
319
|
+
- **Scheduling**: Combine with `schedule()` for time-based queue processing
|
|
320
|
+
- **Retries**: Built-in retry with exponential backoff. See [Retries](./retries.md).
|
|
321
|
+
- **Context**: Queued tasks maintain the original request context
|
|
322
|
+
- **Database**: Uses the same database as other agent data
|
|
323
|
+
|
|
324
|
+
## Limitations
|
|
325
|
+
|
|
326
|
+
- Tasks are processed sequentially, not in parallel
|
|
327
|
+
- No priority system (FIFO only)
|
|
328
|
+
- Queue processing happens during agent execution, not as separate background jobs
|
|
329
|
+
- Failed tasks are dequeued after all retry attempts are exhausted (no dead-letter queue)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Readonly Connections
|
|
2
|
+
|
|
3
|
+
Readonly connections restrict certain WebSocket clients from modifying agent state while still letting them receive state updates and call non-mutating RPC methods.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
When a connection is marked as readonly:
|
|
8
|
+
|
|
9
|
+
- It **receives** state updates from the server
|
|
10
|
+
- It **can call** RPC methods that don't modify state
|
|
11
|
+
- It **cannot** call `this.setState()` — neither via client-side `setState()` nor via a `@callable()` method that calls `this.setState()` internally
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Agent, type Connection, type ConnectionContext } from "agents";
|
|
15
|
+
|
|
16
|
+
export class DocAgent extends Agent<Env, DocState> {
|
|
17
|
+
shouldConnectionBeReadonly(connection: Connection, ctx: ConnectionContext) {
|
|
18
|
+
const url = new URL(ctx.request.url);
|
|
19
|
+
return url.searchParams.get("mode") === "view";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// Client - view-only mode
|
|
26
|
+
const agent = useAgent({
|
|
27
|
+
agent: "DocAgent",
|
|
28
|
+
name: "doc-123",
|
|
29
|
+
query: { mode: "view" },
|
|
30
|
+
onStateUpdateError: (error) => {
|
|
31
|
+
toast.error("You're in view-only mode");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Marking connections as readonly
|
|
37
|
+
|
|
38
|
+
### On connect
|
|
39
|
+
|
|
40
|
+
Override `shouldConnectionBeReadonly` to evaluate each connection when it first connects. Return `true` to mark it readonly.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
export class MyAgent extends Agent<Env, State> {
|
|
44
|
+
shouldConnectionBeReadonly(
|
|
45
|
+
connection: Connection,
|
|
46
|
+
ctx: ConnectionContext
|
|
47
|
+
): boolean {
|
|
48
|
+
const url = new URL(ctx.request.url);
|
|
49
|
+
const role = url.searchParams.get("role");
|
|
50
|
+
return role === "viewer" || role === "guest";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This hook runs before the initial state is sent to the client, so the connection is readonly from the very first message.
|
|
56
|
+
|
|
57
|
+
### At any time
|
|
58
|
+
|
|
59
|
+
Use `setConnectionReadonly` to change a connection's readonly status dynamically:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
export class GameAgent extends Agent<Env, GameState> {
|
|
63
|
+
@callable()
|
|
64
|
+
async startSpectating() {
|
|
65
|
+
const { connection } = getCurrentAgent();
|
|
66
|
+
if (connection) {
|
|
67
|
+
this.setConnectionReadonly(connection, true);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@callable()
|
|
72
|
+
async joinAsPlayer() {
|
|
73
|
+
const { connection } = getCurrentAgent();
|
|
74
|
+
if (connection) {
|
|
75
|
+
this.setConnectionReadonly(connection, false);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Letting a connection toggle its own status
|
|
82
|
+
|
|
83
|
+
A connection can toggle its own readonly status via a callable. This is useful for "lock/unlock" UIs where viewers can opt into editing mode:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { Agent, callable, getCurrentAgent } from "agents";
|
|
87
|
+
|
|
88
|
+
export class CollabAgent extends Agent<Env, State> {
|
|
89
|
+
@callable()
|
|
90
|
+
async setMyReadonly(readonly: boolean) {
|
|
91
|
+
const { connection } = getCurrentAgent();
|
|
92
|
+
if (connection) {
|
|
93
|
+
this.setConnectionReadonly(connection, readonly);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
On the client:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Toggle between readonly and writable
|
|
103
|
+
await agent.call("setMyReadonly", [true]); // lock
|
|
104
|
+
await agent.call("setMyReadonly", [false]); // unlock
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Checking status
|
|
108
|
+
|
|
109
|
+
Use `isConnectionReadonly` to check a connection's current status:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
@callable()
|
|
113
|
+
async getPermissions() {
|
|
114
|
+
const { connection } = getCurrentAgent();
|
|
115
|
+
if (connection) {
|
|
116
|
+
return { canEdit: !this.isConnectionReadonly(connection) };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Handling errors on the client
|
|
122
|
+
|
|
123
|
+
Errors surface in two ways depending on how the write was attempted:
|
|
124
|
+
|
|
125
|
+
- **Client-side `setState()`** — the server sends a `cf_agent_state_error` message. Handle it with the `onStateUpdateError` callback.
|
|
126
|
+
- **`@callable()` methods** — the RPC call rejects with an error. Handle it with a `try`/`catch` around `agent.call()`.
|
|
127
|
+
|
|
128
|
+
> **Note:** `onStateUpdateError` also fires when `validateStateChange` rejects a client-originated state update (with the message `"State update rejected"`). This makes the callback useful for handling any rejected state write, not just readonly errors.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const agent = useAgent({
|
|
132
|
+
agent: "MyAgent",
|
|
133
|
+
name: "instance",
|
|
134
|
+
// Fires when client-side setState() is blocked
|
|
135
|
+
onStateUpdateError: (error) => {
|
|
136
|
+
setError(error);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Fires when a callable that writes state is blocked
|
|
141
|
+
try {
|
|
142
|
+
await agent.call("updateSettings", [newSettings]);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
setError(e instanceof Error ? e.message : String(e)); // "Connection is readonly"
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
To avoid showing errors in the first place, check permissions before rendering edit controls:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
function Editor() {
|
|
152
|
+
const [canEdit, setCanEdit] = useState(false);
|
|
153
|
+
const agent = useAgent({ agent: "MyAgent", name: "instance" });
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
agent.call("getPermissions").then((p) => setCanEdit(p.canEdit));
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
return <button disabled={!canEdit}>{canEdit ? "Edit" : "View Only"}</button>;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## API reference
|
|
164
|
+
|
|
165
|
+
### `shouldConnectionBeReadonly(connection, ctx)`
|
|
166
|
+
|
|
167
|
+
Called when a connection is established. Override to control which connections are readonly.
|
|
168
|
+
|
|
169
|
+
| Parameter | Type | Description |
|
|
170
|
+
| ------------ | ------------------- | ---------------------------- |
|
|
171
|
+
| `connection` | `Connection` | The connecting client |
|
|
172
|
+
| `ctx` | `ConnectionContext` | Contains the upgrade request |
|
|
173
|
+
| **Returns** | `boolean` | `true` to mark as readonly |
|
|
174
|
+
|
|
175
|
+
Default: returns `false` (all connections are writable).
|
|
176
|
+
|
|
177
|
+
### `setConnectionReadonly(connection, readonly?)`
|
|
178
|
+
|
|
179
|
+
Mark or unmark a connection as readonly. Can be called at any time.
|
|
180
|
+
|
|
181
|
+
| Parameter | Type | Description |
|
|
182
|
+
| ------------ | ------------ | ----------------------------------------- |
|
|
183
|
+
| `connection` | `Connection` | The connection to update |
|
|
184
|
+
| `readonly` | `boolean` | `true` to make readonly (default: `true`) |
|
|
185
|
+
|
|
186
|
+
### `isConnectionReadonly(connection)`
|
|
187
|
+
|
|
188
|
+
Check if a connection is currently readonly.
|
|
189
|
+
|
|
190
|
+
| Parameter | Type | Description |
|
|
191
|
+
| ------------ | ------------ | ----------------------- |
|
|
192
|
+
| `connection` | `Connection` | The connection to check |
|
|
193
|
+
| **Returns** | `boolean` | `true` if readonly |
|
|
194
|
+
|
|
195
|
+
### `onStateUpdateError` (client)
|
|
196
|
+
|
|
197
|
+
Callback on `AgentClient` and `useAgent` options. Called when the server rejects a state update.
|
|
198
|
+
|
|
199
|
+
| Parameter | Type | Description |
|
|
200
|
+
| --------- | -------- | ----------------------------- |
|
|
201
|
+
| `error` | `string` | Error message from the server |
|
|
202
|
+
|
|
203
|
+
## How it works
|
|
204
|
+
|
|
205
|
+
Readonly status is stored in the connection's WebSocket attachment, which persists through the WebSocket Hibernation API. The flag is namespaced internally so it cannot be accidentally overwritten by `connection.setState()`. This means:
|
|
206
|
+
|
|
207
|
+
- **Survives hibernation** — the flag is serialized and restored when the agent wakes up
|
|
208
|
+
- **No cleanup needed** — connection state is automatically discarded when the connection closes
|
|
209
|
+
- **Zero overhead** — no database tables or queries, just the connection's built-in attachment
|
|
210
|
+
- **Safe from user code** — `connection.state` and `connection.setState()` never expose or overwrite the readonly flag
|
|
211
|
+
|
|
212
|
+
When a readonly connection tries to modify state, the server blocks it — regardless of whether the write comes from client-side `setState()` or from a `@callable()` method:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
Client (readonly) Agent
|
|
216
|
+
│ │
|
|
217
|
+
│ setState({ count: 1 }) │
|
|
218
|
+
│ ─────────────────────────────▶ │ Check readonly → blocked
|
|
219
|
+
│ ◀─────────────────────────── │
|
|
220
|
+
│ cf_agent_state_error │
|
|
221
|
+
│ │
|
|
222
|
+
│ call("increment") │
|
|
223
|
+
│ ─────────────────────────────▶ │ increment() calls this.setState()
|
|
224
|
+
│ │ Check readonly → throw
|
|
225
|
+
│ ◀─────────────────────────── │
|
|
226
|
+
│ RPC error: "Connection is │
|
|
227
|
+
│ readonly" │
|
|
228
|
+
│ │
|
|
229
|
+
│ call("getPermissions") │
|
|
230
|
+
│ ─────────────────────────────▶ │ getPermissions() — no setState()
|
|
231
|
+
│ ◀─────────────────────────── │
|
|
232
|
+
│ RPC result: { canEdit: false }│
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## What readonly does and does not restrict
|
|
236
|
+
|
|
237
|
+
| Action | Allowed? |
|
|
238
|
+
| ------------------------------------------------------ | -------- |
|
|
239
|
+
| Receive state broadcasts | Yes |
|
|
240
|
+
| Call `@callable()` methods that don't write state | Yes |
|
|
241
|
+
| Call `@callable()` methods that call `this.setState()` | **No** |
|
|
242
|
+
| Send state updates via client-side `setState()` | **No** |
|
|
243
|
+
|
|
244
|
+
The enforcement happens inside `setState()` itself. When a `@callable()` method tries to call `this.setState()` and the current connection context is readonly, the framework throws an `Error("Connection is readonly")`. This means you don't need manual permission checks in your RPC methods — any callable that writes state is automatically blocked for readonly connections.
|
|
245
|
+
|
|
246
|
+
## Caveats
|
|
247
|
+
|
|
248
|
+
### Side effects in callables still run
|
|
249
|
+
|
|
250
|
+
The readonly check happens inside `this.setState()`, not at the start of the callable. If your method has side effects before the state write, those will still execute:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
@callable()
|
|
254
|
+
async processOrder(orderId: string) {
|
|
255
|
+
await sendConfirmationEmail(orderId); // runs even for readonly connections
|
|
256
|
+
await chargePayment(orderId); // runs too
|
|
257
|
+
this.setState({ ...this.state, orders: [...this.state.orders, orderId] }); // throws
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
To avoid this, either check permissions before side effects or structure your code so the state write comes first:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
@callable()
|
|
265
|
+
async processOrder(orderId: string) {
|
|
266
|
+
// Write state first — throws immediately for readonly connections
|
|
267
|
+
this.setState({ ...this.state, orders: [...this.state.orders, orderId] });
|
|
268
|
+
// Side effects only run if setState succeeded
|
|
269
|
+
await sendConfirmationEmail(orderId);
|
|
270
|
+
await chargePayment(orderId);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Related
|
|
275
|
+
|
|
276
|
+
- [State Management](./state.md)
|
|
277
|
+
- [HTTP & WebSockets](./http-websockets.md)
|
|
278
|
+
- [Callable Methods](./callable-methods.md)
|