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.
Files changed (127) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-NofdbL9X.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +595 -137
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +1923 -76
  26. package/dist/chat/index.js +1784 -278
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-FUizKzj2.js → client-BZ-B3NhC.js} +19 -3
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{compaction-helpers-DVcu5lPN.d.ts → compaction-helpers-wUz6M3us.d.ts} +20 -1
  41. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  42. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  43. package/dist/email.js +1 -1
  44. package/dist/email.js.map +1 -1
  45. package/dist/experimental/memory/session/index.d.ts +1 -1
  46. package/dist/experimental/memory/session/index.js +20 -2
  47. package/dist/experimental/memory/session/index.js.map +1 -1
  48. package/dist/experimental/memory/utils/index.d.ts +1 -1
  49. package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
  50. package/dist/index.d.ts +91 -71
  51. package/dist/index.js +562 -24
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/client.d.ts +18 -14
  54. package/dist/mcp/client.js +1 -1
  55. package/dist/mcp/index.d.ts +30 -30
  56. package/dist/mcp/index.js +7 -7
  57. package/dist/mcp/index.js.map +1 -1
  58. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  59. package/dist/message-builder-BymO4N_D.js.map +1 -0
  60. package/dist/observability/index.d.ts +1 -1
  61. package/dist/observability/index.js +4 -2
  62. package/dist/observability/index.js.map +1 -1
  63. package/dist/react.d.ts +123 -110
  64. package/dist/react.js +44 -11
  65. package/dist/react.js.map +1 -1
  66. package/dist/serializable.d.ts +1 -1
  67. package/dist/skills/compile.js +1 -1
  68. package/dist/skills/compile.js.map +1 -1
  69. package/dist/skills/index.js +5 -5
  70. package/dist/skills/index.js.map +1 -1
  71. package/dist/sub-routing.d.ts +6 -6
  72. package/dist/utils.js +1 -1
  73. package/dist/utils.js.map +1 -1
  74. package/dist/vite.js +5 -5
  75. package/dist/vite.js.map +1 -1
  76. package/dist/wire-types-nflOzNuU.js +240 -0
  77. package/dist/wire-types-nflOzNuU.js.map +1 -0
  78. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  79. package/dist/workflow-types.d.ts +25 -21
  80. package/dist/workflow-types.js.map +1 -1
  81. package/dist/workflows.d.ts +22 -21
  82. package/dist/workflows.js +31 -7
  83. package/dist/workflows.js.map +1 -1
  84. package/docs/adding-to-existing-project.md +450 -0
  85. package/docs/agent-class.md +503 -0
  86. package/docs/agent-tools.md +552 -0
  87. package/docs/browse-the-web.md +430 -0
  88. package/docs/callable-methods.md +627 -0
  89. package/docs/chat-agents.md +1687 -0
  90. package/docs/chat-sdk.md +181 -0
  91. package/docs/client-sdk.md +520 -0
  92. package/docs/client-tools-continuation.md +177 -0
  93. package/docs/codemode.md +440 -0
  94. package/docs/configuration.md +775 -0
  95. package/docs/cross-domain-authentication.md +171 -0
  96. package/docs/durable-execution.md +537 -0
  97. package/docs/email.md +663 -0
  98. package/docs/get-current-agent.md +204 -0
  99. package/docs/getting-started.md +305 -0
  100. package/docs/http-websockets.md +668 -0
  101. package/docs/human-in-the-loop.md +661 -0
  102. package/docs/index.md +151 -0
  103. package/docs/long-running-agents.md +730 -0
  104. package/docs/mcp-client.md +620 -0
  105. package/docs/mcp-servers.md +526 -0
  106. package/docs/mcp-transports.md +308 -0
  107. package/docs/migration-to-ai-sdk-v5.md +96 -0
  108. package/docs/migration-to-ai-sdk-v6.md +163 -0
  109. package/docs/observability.md +261 -0
  110. package/docs/push-notifications.md +367 -0
  111. package/docs/queue.md +329 -0
  112. package/docs/readonly-connections.md +278 -0
  113. package/docs/resumable-streaming.md +127 -0
  114. package/docs/retries.md +444 -0
  115. package/docs/routing.md +749 -0
  116. package/docs/scheduling.md +898 -0
  117. package/docs/securing-mcp-servers.md +359 -0
  118. package/docs/server-driven-messages.md +477 -0
  119. package/docs/sessions.md +1024 -0
  120. package/docs/state.md +512 -0
  121. package/docs/sub-agents.md +389 -0
  122. package/docs/webhooks.md +604 -0
  123. package/docs/workflows.md +877 -0
  124. package/package.json +47 -15
  125. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  126. package/dist/agent-tools-DLquv-dp.d.ts +0 -14
  127. 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)