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
@@ -0,0 +1,477 @@
1
+ # Trigger patterns
2
+
3
+ Send messages and trigger LLM responses from the server without a human action. Use this for scheduled follow-ups, queue processing, email-triggered responses, and autonomous agent workflows.
4
+
5
+ ## Overview
6
+
7
+ In a typical chat flow, the user sends a message and the agent responds. But agents often need to act on their own — a scheduled reminder fires, a webhook arrives, a workflow completes, or the agent decides to continue after inspecting its own response.
8
+
9
+ The key primitives:
10
+
11
+ | Primitive | Role |
12
+ | ------------------- | ---------------------------------------------------------------------------------- |
13
+ | `saveMessages` | Inject a message and trigger the LLM — the server-side equivalent of `sendMessage` |
14
+ | `submitMessages` | Durably accept a Think turn for async execution and inspect it later |
15
+ | `persistMessages` | Store messages without triggering a response — for injecting context silently |
16
+ | `onChatResponse` | React when any response completes, including ones you did not initiate |
17
+ | `isServerStreaming` | Client-side flag: `true` when a server-initiated stream is active |
18
+
19
+ ### `saveMessages` vs `persistMessages`
20
+
21
+ `saveMessages` persists messages to SQLite **and** triggers `onChatMessage` for a new LLM response. It is awaitable — after it returns, the LLM has responded and the message is persisted.
22
+
23
+ `persistMessages` stores messages and broadcasts them to connected clients, but does **not** trigger a model turn. Use it when you want to inject context (for example, a system message or background data) into the conversation without starting a response.
24
+
25
+ ### `saveMessages` vs `submitMessages`
26
+
27
+ Use `saveMessages()` when the caller can wait for the model turn to finish.
28
+
29
+ Use `submitMessages()` when the caller needs a fast durable receipt, idempotent retry, and later status inspection. This is useful for webhook handlers, RPC callers, and parent Workers with strict timeout limits:
30
+
31
+ ```typescript
32
+ const submission = await this.submitMessages(
33
+ [
34
+ {
35
+ id: crypto.randomUUID(),
36
+ role: "user",
37
+ parts: [
38
+ { type: "text", text: `Webhook event: ${JSON.stringify(payload)}` }
39
+ ]
40
+ }
41
+ ],
42
+ { idempotencyKey: payload.id }
43
+ );
44
+
45
+ return Response.json({
46
+ submissionId: submission.submissionId,
47
+ status: submission.status,
48
+ accepted: submission.accepted
49
+ });
50
+ ```
51
+
52
+ `submitMessages()` stores pending work first and appends the messages to the conversation `Session` only when the submission starts executing. It accepts serializable `UIMessage[]` values, not the function form supported by `saveMessages((messages) => ...)`.
53
+
54
+ Use [`startFiber()`](./durable-execution.md#startfiber) outside Think when the
55
+ durable unit is a surrounding application job, such as accepting a webhook once,
56
+ restoring provider state, posting a visible reply, and recording recovery
57
+ policy. `submitMessages()` owns Think's conversation admission; managed fibers
58
+ own external side effects around that turn.
59
+
60
+ For Think-specific guidance that also compares raw child `chat()` calls with
61
+ agent tools, see [Choosing a turn API](https://github.com/cloudflare/agents/blob/main/docs/think/index.md#choosing-a-turn-api).
62
+
63
+ ### When to use `saveMessages` vs `onChatResponse`
64
+
65
+ **Use `saveMessages` when you control the trigger** — schedule callbacks, webhooks, email handlers, or any method where you decide when to inject a message.
66
+
67
+ **Use `onChatResponse` when you need to react to responses you did not trigger** — user-initiated messages, auto-continuations after tool approvals, or any turn that the framework ran on your behalf.
68
+
69
+ ## `waitUntilStable`
70
+
71
+ Always call `waitUntilStable()` before reading `this.messages` or calling `saveMessages` from schedule callbacks, webhooks, email handlers, or other non-chat entry points.
72
+
73
+ `waitUntilStable()` waits until the conversation is fully stable:
74
+
75
+ - No active LLM stream in progress
76
+ - No pending client-tool interactions (tool results or approvals the user has not yet provided)
77
+ - No queued continuation turns
78
+
79
+ It returns `true` when stable, or `false` if the timeout expires before a pending interaction resolves. If nothing is pending, it returns immediately.
80
+
81
+ ```typescript
82
+ const stable = await this.waitUntilStable({ timeout: 30_000 });
83
+ if (!stable) {
84
+ // The conversation is blocked on a user interaction or an in-flight
85
+ // stream that did not complete within 30 seconds.
86
+ console.warn("Conversation not stable, skipping server-driven message");
87
+ return;
88
+ }
89
+ // Safe to read this.messages and call saveMessages.
90
+ ```
91
+
92
+ Without this guard, you risk reading stale messages or overlapping with an in-flight stream.
93
+
94
+ ## Triggering responses from the server
95
+
96
+ ### Cron schedule
97
+
98
+ A daily digest agent that summarizes activity every morning. Cron schedules are idempotent by default, so calling `schedule()` in `onStart` is safe — it will not create duplicates across Durable Object restarts.
99
+
100
+ ```typescript
101
+ import { AIChatAgent } from "@cloudflare/ai-chat";
102
+
103
+ export class DigestAgent extends AIChatAgent {
104
+ async onChatMessage() {
105
+ // ... your LLM call
106
+ }
107
+
108
+ async onStart() {
109
+ await this.schedule("0 9 * * *", "dailyDigest");
110
+ }
111
+
112
+ async dailyDigest() {
113
+ const stable = await this.waitUntilStable({ timeout: 30_000 });
114
+ if (!stable) {
115
+ console.warn("Conversation not stable, skipping daily digest");
116
+ return;
117
+ }
118
+
119
+ await this.saveMessages((messages) => [
120
+ ...messages,
121
+ {
122
+ id: crypto.randomUUID(),
123
+ role: "user",
124
+ parts: [
125
+ {
126
+ type: "text",
127
+ text: "Summarize what happened since your last digest."
128
+ }
129
+ ],
130
+ createdAt: new Date()
131
+ }
132
+ ]);
133
+ // At this point the LLM has responded and the message is persisted.
134
+ }
135
+ }
136
+ ```
137
+
138
+ The function form of `saveMessages` — `saveMessages((messages) => [...])` — reads the latest persisted messages at execution time. This avoids stale baselines when multiple calls queue up (for example, rapid webhook arrivals). See [scheduling](./scheduling.md) for more on `schedule()` and cron syntax.
139
+
140
+ ### Processing a queue
141
+
142
+ When you control the trigger, a simple loop is the clearest pattern:
143
+
144
+ ```typescript
145
+ async processQueue() {
146
+ for (const task of this.taskQueue) {
147
+ const stable = await this.waitUntilStable({ timeout: 30_000 });
148
+ if (!stable) {
149
+ console.warn("Conversation not stable, stopping queue processing");
150
+ break;
151
+ }
152
+
153
+ await this.saveMessages((messages) => [
154
+ ...messages,
155
+ {
156
+ id: crypto.randomUUID(),
157
+ role: "user",
158
+ parts: [{ type: "text", text: task }],
159
+ createdAt: new Date()
160
+ }
161
+ ]);
162
+ // LLM has responded. this.messages is updated. Next iteration.
163
+ }
164
+ this.taskQueue = [];
165
+ }
166
+ ```
167
+
168
+ No special hooks needed — `saveMessages` returns after the full turn completes.
169
+
170
+ ### Email-triggered
171
+
172
+ ```typescript
173
+ async onEmail(email: AgentEmail) {
174
+ const stable = await this.waitUntilStable({ timeout: 30_000 });
175
+ if (!stable) {
176
+ console.warn("Conversation not stable, cannot process email");
177
+ return;
178
+ }
179
+
180
+ const subject = email.headers.get("subject") ?? "(no subject)";
181
+ const body = await new Response(email.raw).text();
182
+
183
+ await this.saveMessages((messages) => [
184
+ ...messages,
185
+ {
186
+ id: crypto.randomUUID(),
187
+ role: "user",
188
+ parts: [
189
+ {
190
+ type: "text",
191
+ text: `Email from ${email.from}: ${subject}\n\n${body}`
192
+ }
193
+ ],
194
+ createdAt: new Date()
195
+ }
196
+ ]);
197
+ }
198
+ ```
199
+
200
+ ### Webhook-triggered
201
+
202
+ ```typescript
203
+ async onRequest(request: Request): Promise<Response> {
204
+ const url = new URL(request.url);
205
+
206
+ if (url.pathname.endsWith("/webhook") && request.method === "POST") {
207
+ const stable = await this.waitUntilStable({ timeout: 30_000 });
208
+ if (!stable) {
209
+ return new Response("Agent is busy", { status: 503 });
210
+ }
211
+
212
+ const payload = await request.json();
213
+ try {
214
+ await this.saveMessages((messages) => [
215
+ ...messages,
216
+ {
217
+ id: crypto.randomUUID(),
218
+ role: "user",
219
+ parts: [
220
+ { type: "text", text: `Webhook event: ${JSON.stringify(payload)}` }
221
+ ],
222
+ createdAt: new Date()
223
+ }
224
+ ]);
225
+ return new Response("ok");
226
+ } catch (error) {
227
+ console.error("Failed to process webhook:", error);
228
+ return new Response("Internal error", { status: 500 });
229
+ }
230
+ }
231
+
232
+ return super.onRequest(request);
233
+ }
234
+ ```
235
+
236
+ ### Injecting context without triggering a response
237
+
238
+ Use `persistMessages` to add messages that the LLM will see on its next turn, without starting a turn now:
239
+
240
+ ```typescript
241
+ async addBackgroundContext(data: string) {
242
+ const stable = await this.waitUntilStable({ timeout: 30_000 });
243
+ if (!stable) return;
244
+
245
+ await this.persistMessages([
246
+ ...this.messages,
247
+ {
248
+ id: crypto.randomUUID(),
249
+ role: "user",
250
+ parts: [
251
+ { type: "text", text: `[Background context]: ${data}` }
252
+ ],
253
+ createdAt: new Date()
254
+ }
255
+ ]);
256
+ // Message is stored and broadcast to clients, but no LLM call happens.
257
+ }
258
+ ```
259
+
260
+ ## Reacting to responses you did not initiate
261
+
262
+ `onChatResponse` fires after **every** completed turn — user-initiated messages, `saveMessages` calls, and auto-continuations. Use it when you need to observe or react to responses regardless of how they were triggered.
263
+
264
+ ### Broadcasting state
265
+
266
+ ```typescript
267
+ import { AIChatAgent, type ChatResponseResult } from "@cloudflare/ai-chat";
268
+
269
+ export class ChatAgent extends AIChatAgent {
270
+ async onChatMessage() {
271
+ // ... your LLM call
272
+ }
273
+
274
+ protected async onChatResponse(result: ChatResponseResult) {
275
+ if (result.status === "completed") {
276
+ this.broadcast(JSON.stringify({ streaming: false }));
277
+ }
278
+ }
279
+ }
280
+ ```
281
+
282
+ ### Analytics
283
+
284
+ ```typescript
285
+ protected async onChatResponse(result: ChatResponseResult) {
286
+ try {
287
+ await fetch("https://analytics.example.com/event", {
288
+ method: "POST",
289
+ body: JSON.stringify({
290
+ requestId: result.requestId,
291
+ status: result.status,
292
+ continuation: result.continuation
293
+ })
294
+ });
295
+ } catch (error) {
296
+ console.error("Analytics reporting failed:", error);
297
+ }
298
+ }
299
+ ```
300
+
301
+ ### Chained reasoning
302
+
303
+ An agent can inspect its own response and decide whether to continue. This works for user-initiated messages too — you cannot predict what the user will ask, but you can react to what the agent said.
304
+
305
+ ```typescript
306
+ protected async onChatResponse(result: ChatResponseResult) {
307
+ if (result.status !== "completed") return;
308
+
309
+ const lastText = result.message.parts
310
+ .filter((p) => p.type === "text")
311
+ .map((p) => p.text)
312
+ .join("");
313
+
314
+ if (lastText.includes("[NEEDS_MORE_RESEARCH]")) {
315
+ await this.saveMessages((messages) => [
316
+ ...messages,
317
+ {
318
+ id: crypto.randomUUID(),
319
+ role: "user",
320
+ parts: [{ type: "text", text: "Continue your research." }],
321
+ createdAt: new Date()
322
+ }
323
+ ]);
324
+ }
325
+ }
326
+ ```
327
+
328
+ When `saveMessages` is called from inside `onChatResponse`, the inner turn runs to completion and `saveMessages` returns. After the current `onChatResponse` call returns, the framework fires `onChatResponse` again for the inner response. This continues until no more work is queued. The framework never nests `onChatResponse` calls — results are drained sequentially.
329
+
330
+ ### Reactive queue processing
331
+
332
+ When queue items can be added by external events (user messages, webhooks) at any time, `onChatResponse` lets you drain the queue after every response regardless of who triggered it:
333
+
334
+ ```typescript
335
+ protected async onChatResponse(result: ChatResponseResult) {
336
+ if (result.status === "completed" && this.taskQueue.length > 0) {
337
+ const next = this.taskQueue.shift()!;
338
+ await this.saveMessages((messages) => [
339
+ ...messages,
340
+ {
341
+ id: crypto.randomUUID(),
342
+ role: "user",
343
+ parts: [{ type: "text", text: next }],
344
+ createdAt: new Date()
345
+ }
346
+ ]);
347
+ }
348
+ }
349
+ ```
350
+
351
+ ### `ChatResponseResult` fields
352
+
353
+ | Field | Type | Description |
354
+ | -------------- | ------------------------------------- | ---------------------------------------- |
355
+ | `message` | `UIMessage` | The finalized assistant message |
356
+ | `requestId` | `string` | Unique ID for this turn |
357
+ | `continuation` | `boolean` | `true` if this was an auto-continuation |
358
+ | `status` | `"completed" \| "error" \| "aborted"` | How the turn ended |
359
+ | `error` | `string \| undefined` | Error details when `status` is `"error"` |
360
+
361
+ ## Client-side: detecting server-initiated streams
362
+
363
+ When the server triggers a stream via `saveMessages`, the AI SDK's `status` stays `"ready"` because the client did not initiate the request. The `useAgentChat` hook provides two additional flags to handle this:
364
+
365
+ | Flag | What it tracks |
366
+ | ------------------- | --------------------------------------------------------------------------------------------------------- |
367
+ | `status` | AI SDK lifecycle: `"submitted"`, `"streaming"`, `"ready"`, `"error"` — only for client-initiated requests |
368
+ | `isServerStreaming` | `true` when a server-initiated stream is active |
369
+ | `isStreaming` | `true` when either client or server streaming is active — use this for a universal indicator |
370
+
371
+ Use `isStreaming` for most UI concerns (disabling the send button, showing a loading indicator). Use `isServerStreaming` only when you need to distinguish between user-initiated and server-initiated streams (for example, to show a different indicator like "Agent is working in the background...").
372
+
373
+ ```tsx
374
+ import { useAgent } from "agents/react";
375
+ import { useAgentChat } from "@cloudflare/ai-chat/react";
376
+
377
+ function Chat() {
378
+ const agent = useAgent({ agent: "ChatAgent" });
379
+ const { messages, sendMessage, isStreaming, isServerStreaming } =
380
+ useAgentChat({ agent });
381
+
382
+ return (
383
+ <div>
384
+ {messages.map((m) => (
385
+ <div key={m.id}>{/* render message */}</div>
386
+ ))}
387
+
388
+ {isServerStreaming && <div>Agent is working in the background...</div>}
389
+ {!isServerStreaming && isStreaming && <div>Agent is responding...</div>}
390
+
391
+ <form
392
+ onSubmit={(e) => {
393
+ e.preventDefault();
394
+ const input = e.currentTarget.elements.namedItem(
395
+ "input"
396
+ ) as HTMLInputElement;
397
+ sendMessage({ text: input.value });
398
+ input.value = "";
399
+ }}
400
+ >
401
+ <input name="input" placeholder="Type a message..." />
402
+ <button type="submit" disabled={isStreaming}>
403
+ Send
404
+ </button>
405
+ </form>
406
+ </div>
407
+ );
408
+ }
409
+ ```
410
+
411
+ When a server-driven response arrives while the user is idle, connected clients see the new messages appear in real time. The `isStreaming` flag transitions from `false` → `true` → `false` as the stream runs, so UI elements like the send button automatically disable and re-enable.
412
+
413
+ ## Interaction with `messageConcurrency`
414
+
415
+ The `messageConcurrency` setting on `AIChatAgent` controls how overlapping user submissions behave (`"queue"`, `"latest"`, `"merge"`, `"drop"`, `"debounce"`). This setting only applies to `sendMessage()` — user-initiated messages from the client.
416
+
417
+ `saveMessages()` always uses serialized (queued) behavior regardless of the `messageConcurrency` setting. This means server-driven messages never get dropped, merged, or debounced — they always queue up and execute in order.
418
+
419
+ ## Combining with other Agent primitives
420
+
421
+ | Primitive | How to combine |
422
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------- |
423
+ | `schedule()` | Schedule a callback that calls `saveMessages` — see the cron example above |
424
+ | `submitMessages()` | Durably accept a Think turn when the caller cannot wait for `saveMessages()` to finish |
425
+ | `queue()` | Queue a method that calls `saveMessages` for deferred processing |
426
+ | `runWorkflow()` | Start a Workflow; use `AgentWorkflow.agent` RPC to call a method that triggers `saveMessages` or `submitMessages` |
427
+ | `onEmail()` | Convert email content to a chat message and call `saveMessages` |
428
+ | `onRequest()` | Handle webhooks and call `saveMessages` |
429
+ | `this.broadcast()` | Broadcast custom state from `onChatResponse` |
430
+
431
+ ## Cancelling a server-driven turn
432
+
433
+ Pass `options.signal` to cancel a programmatic turn from outside without knowing the internally-generated request id:
434
+
435
+ ```typescript
436
+ async runLongTask(query: string, abortSignal: AbortSignal) {
437
+ const result = await this.saveMessages(
438
+ [{ id: crypto.randomUUID(), role: "user", parts: [{ type: "text", text: query }] }],
439
+ { signal: abortSignal }
440
+ );
441
+
442
+ if (result.status === "aborted") {
443
+ // The signal aborted mid-stream. Partial chunks are still persisted.
444
+ }
445
+ }
446
+ ```
447
+
448
+ When the signal aborts:
449
+
450
+ - the inference loop's signal aborts (same path `chat-request-cancel` takes);
451
+ - partial chunks streamed before the abort are persisted;
452
+ - `saveMessages` resolves with `{ status: "aborted" }`;
453
+ - `onChatResponse` fires with `status: "aborted"`.
454
+
455
+ Pre-aborted signals short-circuit before any model work runs.
456
+
457
+ ### Limitations
458
+
459
+ - **Signals cannot cross Durable Object boundaries.** `AbortSignal` is not an RPC-serializable type. Construct the controller inside the DO that calls `saveMessages`. For Think child-agent orchestration, use [Agent Tools](./agent-tools.md); `runAgentTool()` bridges parent aborts into the child run. For lower-level custom RPC, return a `ReadableStream` from the child and let the parent cancel it — workerd propagates the cancel back to the source's `cancel` callback.
460
+ - **Hibernation drops the listener.** The signal lives in memory. If the DO hibernates mid-turn and `chatRecovery` is enabled, the recovered turn usually calls `continueLastTurn()` internally without the original signal — an abort fired after restart has no effect on the recovered turn. For pre-stream interruptions, recovery can instead retry the latest unanswered user message automatically. This is true for top-level agents and sub-agents; sub-agent recovery still works, but the original caller's in-memory signal is gone. Override `onChatRecovery` (Think) or set `chatRecovery = false` for callers that need stronger guarantees.
461
+
462
+ This is the integration point for agent-tool orchestration where the parent's AI SDK abort signal needs to propagate into a child DO's `saveMessages` call. See [`cloudflare/agents#1406`](https://github.com/cloudflare/agents/issues/1406) for the original use case.
463
+
464
+ ## Important notes
465
+
466
+ - **`saveMessages` is awaitable.** After it returns, the LLM has responded and the message is persisted. Use this when you control the trigger.
467
+ - **`submitMessages` is durable admission.** It returns after the turn is accepted, not after the LLM responds. Use it when timeout ambiguity would make retries unsafe.
468
+ - **Use the function form of `saveMessages`.** `saveMessages((messages) => [...messages, newMsg])` reads the latest persisted messages at execution time, avoiding stale baselines when multiple calls queue up.
469
+ - **`submitMessages` accepts serializable messages.** It takes `UIMessage[]` so accepted work can be stored durably before execution.
470
+ - **`persistMessages` does not trigger a response.** Use it to inject context or system messages silently.
471
+ - **`onChatResponse` is for reacting to turns you did not initiate.** Use it for user-initiated messages, auto-continuations, or any turn where you did not call `saveMessages` yourself.
472
+ - **`onChatResponse` does not nest.** When `saveMessages` is called from inside `onChatResponse`, the inner turn completes and `onChatResponse` fires again sequentially — not recursively.
473
+ - **Messages are persisted before `onChatResponse` fires.** If the Durable Object evicts during the hook, the conversation is safe in SQLite — only the hook callback is lost.
474
+ - **`waitUntilStable()` before injecting.** Always call this from schedule callbacks, webhooks, or other non-chat entry points to avoid overlapping with an in-flight stream or pending tool interaction.
475
+ - **The client sees `done: true` before `onChatResponse` runs.** The server-side hook does not delay the client.
476
+ - **`saveMessages` accepts `options.signal` for external cancellation.** Useful when forwarding an upstream `AbortSignal` (e.g. from an AI SDK tool `execute` on a parent agent) into a child DO's chat turn.
477
+ - **`messageConcurrency` does not affect `saveMessages`.** Server-driven messages always queue and execute in order.