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,552 @@
1
+ # Agent Tools
2
+
3
+ Agent tools let one chat agent dispatch another chat-capable sub-agent as part
4
+ of its work. The child is a real sub-agent with its own Durable Object storage,
5
+ messages, tools, resumable stream, and drill-in URL. The parent keeps a small
6
+ run registry so clients can render the child timeline, replay it after refresh,
7
+ and clean it up later.
8
+
9
+ Agent tools support `@cloudflare/think` agents and `AIChatAgent` subclasses.
10
+ `AIChatAgent` children run headlessly through `saveMessages()`, so they should
11
+ use server-side tools. Browser-provided client tools are not available during an
12
+ agent-tool turn unless you model that interaction as server-side state or a
13
+ separate parent-mediated workflow.
14
+
15
+ For Think children, prefer agent tools when the parent model or workflow
16
+ delegates work and you want retained child runs, event replay, abort bridging,
17
+ and UI drill-in. Use raw `subAgent(...).chat()` only for lower-level streaming
18
+ RPC where your code owns forwarding, cancellation, and replay policy. For the
19
+ full comparison, see [Choosing a turn API](https://github.com/cloudflare/agents/blob/main/docs/think/index.md#choosing-a-turn-api).
20
+
21
+ ## Use an Agent as an AI SDK tool
22
+
23
+ Use `agentTool()` when the parent model should decide when to call the helper.
24
+
25
+ ```ts
26
+ import { Think } from "@cloudflare/think";
27
+ import { agentTool } from "agents/agent-tools";
28
+ import { z } from "zod";
29
+
30
+ export class Researcher extends Think<Env> {
31
+ getSystemPrompt() {
32
+ return "Research the user's topic and end with a concise summary.";
33
+ }
34
+ }
35
+
36
+ export class Assistant extends Think<Env> {
37
+ getTools() {
38
+ return {
39
+ research: agentTool(Researcher, {
40
+ description: "Research one topic in depth.",
41
+ displayName: "Researcher",
42
+ inputSchema: z.object({
43
+ query: z.string().min(3)
44
+ })
45
+ })
46
+ };
47
+ }
48
+ }
49
+ ```
50
+
51
+ The child can also be an `AIChatAgent`:
52
+
53
+ ```ts
54
+ import { AIChatAgent } from "@cloudflare/ai-chat";
55
+ import { agentTool } from "agents/agent-tools";
56
+ import { convertToModelMessages, stepCountIs, streamText } from "ai";
57
+ import { z } from "zod";
58
+
59
+ export class Summarizer extends AIChatAgent<Env> {
60
+ protected override formatAgentToolInput(input: { text: string }, request) {
61
+ return {
62
+ id: `agent-tool-${request.runId}-input`,
63
+ role: "user",
64
+ parts: [{ type: "text", text: `Summarize:\n\n${input.text}` }]
65
+ };
66
+ }
67
+
68
+ async onChatMessage() {
69
+ const result = streamText({
70
+ model: this.env.MODEL,
71
+ messages: await convertToModelMessages(this.messages)
72
+ });
73
+ return result.toUIMessageStreamResponse();
74
+ }
75
+ }
76
+
77
+ export class Assistant extends AIChatAgent<Env> {
78
+ async onChatMessage() {
79
+ const result = streamText({
80
+ model: this.env.MODEL,
81
+ messages: await convertToModelMessages(this.messages),
82
+ tools: {
83
+ summarize: agentTool(Summarizer, {
84
+ description: "Summarize long text in a separate retained agent.",
85
+ inputSchema: z.object({ text: z.string() })
86
+ })
87
+ },
88
+ stopWhen: stepCountIs(5)
89
+ });
90
+ return result.toUIMessageStreamResponse();
91
+ }
92
+ }
93
+ ```
94
+
95
+ The generated tool calls `this.runAgentTool(ChildAgent, ...)`, streams
96
+ `agent-tool-event` frames on the parent WebSocket, and returns the child
97
+ summary to the parent model. If the run fails, aborts, or is interrupted, the
98
+ tool returns a structured `AgentToolFailure` instead of an empty success value:
99
+
100
+ ```ts
101
+ type AgentToolFailure = {
102
+ ok: false;
103
+ status: "error" | "aborted" | "interrupted";
104
+ error: string; // human-readable, safe to surface
105
+ retryable: boolean;
106
+ // Present only when `status` is "interrupted":
107
+ reason?: AgentToolInterruptedReason;
108
+ childStillRunning?: boolean;
109
+ };
110
+
111
+ type AgentToolInterruptedReason =
112
+ | "no-progress"
113
+ | "window-exceeded"
114
+ | "not-tailable"
115
+ | "inspect-timeout"
116
+ | "inspect-failed"
117
+ | "recovery-deadline"
118
+ | "budget-exceeded";
119
+ ```
120
+
121
+ `retryable` is `true` only for an `interrupted` run — the child was reset or
122
+ superseded by a deploy or parent recovery and never reached a logical outcome,
123
+ so re-dispatching the same call can succeed. A genuine `error` or an intentional
124
+ `aborted` is `retryable: false`. This lets a parent prompt convention or an
125
+ orchestration harness re-run a transient interruption rather than reporting it
126
+ to the user as a final failure. `AgentToolFailure` is exported from `agents`.
127
+
128
+ On an `interrupted` run, `reason` gives a machine-readable cause and
129
+ `childStillRunning` reports whether the child was still working when the parent
130
+ stopped waiting (`true`) or has since been torn down (`false`). Branch on these
131
+ instead of parsing the `error` prose — for example, re-dispatch a `no-progress`
132
+ interrupt (the child may still self-heal) but reconnect to or surface a
133
+ `window-exceeded` one (the child was torn down). Both `reason` and
134
+ `childStillRunning` are also mirrored onto the `agent-tool-event` wire frame and
135
+ the `useAgentToolEvents()` run state.
136
+
137
+ For `Think` children that do workflow-style work without user-facing assistant
138
+ text, override `getAgentToolOutput()` and, if needed, `getAgentToolSummary()`.
139
+ Assistant text remains the default summary when present, but a Think agent-tool
140
+ run can complete successfully without emitting text chunks.
141
+ Persist any structured output before the child turn finishes, because
142
+ `getAgentToolOutput()` is read as soon as `saveMessages()` resolves. Keep
143
+ `getAgentToolSummary()` concise for display; the full structured value is stored
144
+ separately as the tool output.
145
+
146
+ ```ts
147
+ export class Extractor extends Think<Env> {
148
+ protected override getAgentToolOutput(runId: string) {
149
+ const rows = this.sql<{ result_json: string }>`
150
+ SELECT result_json FROM extraction_runs WHERE id = ${runId}
151
+ `;
152
+ return rows[0] ? JSON.parse(rows[0].result_json) : undefined;
153
+ }
154
+
155
+ protected override getAgentToolSummary(_runId: string, output: unknown) {
156
+ return output ? "Extraction complete" : "";
157
+ }
158
+ }
159
+ ```
160
+
161
+ ## Run an Agent tool imperatively
162
+
163
+ Use `runAgentTool()` for deterministic workflows, scheduled work, HTTP
164
+ handlers, or fan-out code.
165
+
166
+ ```ts
167
+ const [a, b] = await Promise.allSettled([
168
+ this.runAgentTool(Researcher, {
169
+ input: { query: "HTTP/3" },
170
+ parentToolCallId: toolCallId,
171
+ displayOrder: 0
172
+ }),
173
+ this.runAgentTool(Researcher, {
174
+ input: { query: "gRPC" },
175
+ parentToolCallId: toolCallId,
176
+ displayOrder: 1
177
+ })
178
+ ]);
179
+ ```
180
+
181
+ `runAgentTool()` is idempotent by `runId`. Passing the same `runId` never starts
182
+ a duplicate child turn. Completed, failed, aborted, and interrupted runs are
183
+ retained until you explicitly clear them.
184
+
185
+ ## Detached (background) runs
186
+
187
+ By default `runAgentTool()` **awaits** the child to terminal before returning.
188
+ For long-running work — large imports, video renders, deep research — that you
189
+ do not want to block the dispatching turn on, pass `detached`. The run is
190
+ dispatched, the current turn continues, and `runAgentTool()` returns a handle
191
+ immediately:
192
+
193
+ ```ts
194
+ type DetachedRunAgentToolResult = {
195
+ runId: string;
196
+ agentType: string;
197
+ status: "running" | "error"; // "error" only if dispatch itself was rejected
198
+ };
199
+ ```
200
+
201
+ `detached: true` is fire-and-forget — observe the run through `agent-tool-event`
202
+ frames (the same ones `useAgentToolEvents()` consumes) and the global
203
+ `onAgentToolFinish()` hook. Pass an object to wire a targeted, durable
204
+ completion callback:
205
+
206
+ ```ts
207
+ async startImport(input: ImportInput) {
208
+ const { runId } = await this.runAgentTool(ImportAgent, {
209
+ input,
210
+ detached: { onFinish: "onImportDone", maxBudgetMs: 60 * 60 * 1000 }
211
+ });
212
+ return runId;
213
+ }
214
+
215
+ // Fires once, even if the Durable Object was evicted and rehydrated while the
216
+ // child ran. Referenced by METHOD NAME (like schedule()) — never a closure,
217
+ // which cannot survive eviction.
218
+ async onImportDone(run: AgentToolRunInfo, result: AgentToolLifecycleResult) {
219
+ switch (result.status) {
220
+ case "completed":
221
+ await this.markImportReady(run.runId, result.summary);
222
+ break;
223
+ case "error":
224
+ await this.markImportFailed(run.runId, result.error);
225
+ break;
226
+ case "interrupted":
227
+ // reason "budget-exceeded" ⇒ the run hit its maxBudgetMs ceiling.
228
+ // interrupted is soft: a child that finishes anyway re-fires this hook
229
+ // with "completed", so make the handler idempotent.
230
+ break;
231
+ }
232
+ }
233
+ ```
234
+
235
+ Key behaviors:
236
+
237
+ - **Durable completion.** Delivery survives eviction and deploys: a warm fast
238
+ path delivers with low latency while the isolate is alive, and a
239
+ self-scheduling reconcile backbone finalizes anything the fast path missed.
240
+ Delivery is exactly-once on the happy path; under a crash it is at-least-once,
241
+ so `onFinish` handlers must be idempotent.
242
+ - **Give-up vs. finish are independent.** A budget give-up is delivered as
243
+ `status: "interrupted"`, `reason: "budget-exceeded"`. Because `interrupted` is
244
+ soft, a child that completes after the give-up still re-fires `onFinish` with
245
+ the real result — a premature give-up never hides a late completion.
246
+ - **Bounded.** Every detached run has an absolute `maxBudgetMs` ceiling
247
+ (per-run, or the `detachedMaxBudgetMs` static option; default 24h). On expiry
248
+ the parent gives up watching and tears the child down so an abandoned run
249
+ cannot hold a `maxConcurrentAgentTools` slot forever.
250
+ - **No inherited signal.** A detached run must outlive the spawning turn, so it
251
+ does **not** inherit `options.signal`. Cancel it explicitly:
252
+
253
+ ```ts
254
+ await this.cancelAgentTool(runId); // idempotent; delivers onFinish "aborted"
255
+ ```
256
+
257
+ ### Notify the chat on completion (Think / AIChatAgent)
258
+
259
+ On a chat agent (`@cloudflare/think` or `AIChatAgent`) you usually want the model
260
+ to _react_ to a finished background run. Instead of wiring `onFinish` by hand,
261
+ pass `notify: true` — when the run finishes the agent injects a message into the
262
+ chat (idempotent per run + status, so an exactly-once finish never duplicates)
263
+ and the model takes its next turn with the result in context:
264
+
265
+ ```ts
266
+ await this.runAgentTool(ResearchAgent, { input, detached: { notify: true } });
267
+ ```
268
+
269
+ If your app routes or hides synthetic messages by `metadata.source`, pass your
270
+ own source:
271
+
272
+ ```ts
273
+ await this.runAgentTool(ResearchAgent, {
274
+ input,
275
+ detached: { notify: { source: "research-background" } }
276
+ });
277
+ ```
278
+
279
+ Override `formatDetachedCompletion(run, result)` to customize the injected text,
280
+ or return an empty string to suppress the notification for a given outcome. An
281
+ explicit `onFinish` takes precedence over `notify`.
282
+
283
+ ### The `inspectAgentToolRun` contract
284
+
285
+ A child's `inspectAgentToolRun(runId)` returns the run's current status snapshot,
286
+ or `null`. **`null` does not mean "failed"** — it means the child has no record
287
+ of that run _yet_. This is normal immediately after dispatch (the child may
288
+ still be persisting its first row) and is also what a freshly-rehydrated child
289
+ returns before it has lazily reconciled a stale `running` row. Callers — and the
290
+ framework's own reconcile backbone — treat `null` as "not terminal, keep
291
+ watching within budget", never as a terminal failure. Only a non-`null`
292
+ inspection with a terminal `status` (`completed` / `error` / `aborted`)
293
+ finalizes a run.
294
+
295
+ ## Report progress and milestones
296
+
297
+ A sub-agent running as an agent tool — awaited or detached — can report mid-run
298
+ progress so a parent can render a live status line, meter the run server-side, or
299
+ react to a named checkpoint before the run finishes. Call `reportProgress()` from
300
+ inside the child (for example, from a tool's `execute`):
301
+
302
+ ```ts
303
+ export class ImportAgent extends Think<Env> {
304
+ getTools() {
305
+ return {
306
+ ingest: tool({
307
+ inputSchema: z.object({ url: z.string() }),
308
+ execute: async ({ url }) => {
309
+ // Ephemeral progress: drives a generic bar / phase / status line.
310
+ await this.reportProgress({
311
+ fraction: 0.6,
312
+ phase: "ingesting",
313
+ message: "Ingested 40k/80k rows"
314
+ });
315
+ // ...
316
+ }
317
+ })
318
+ };
319
+ }
320
+ }
321
+ ```
322
+
323
+ `reportProgress()` is available on chat agents (`@cloudflare/think` and
324
+ `AIChatAgent`). It is a no-op with a development warning on the base `Agent`
325
+ class and when called outside an active agent-tool run, so the same child code is
326
+ safe to run standalone. The framework resolves the active run from the current
327
+ turn — you never thread a run id.
328
+
329
+ ```ts
330
+ reportProgress<T>(
331
+ progress: {
332
+ fraction?: number; // 0..1 — drives a progress bar
333
+ message?: string; // human-readable status line
334
+ phase?: string; // coarse phase label, e.g. "ingesting"
335
+ milestone?: string; // present ⇒ a durable milestone (see below)
336
+ data?: T; // app-specific payload; live-only unless persisted
337
+ },
338
+ options?: { persist?: boolean }
339
+ ): Promise<void>;
340
+ ```
341
+
342
+ Ephemeral signals ride the child's own turn stream as a transient
343
+ `data-agent-progress` part, so they re-broadcast to the parent's connected
344
+ clients and surface on `AgentToolRunState.progress` through `useAgentToolEvents()`
345
+ — a background-runs tray can render a live bar, phase, and status line without
346
+ drilling in. Bursts are coalesced (latest-wins; a `fraction >= 1` frame always
347
+ flushes). The `data` field is live-only unless you pass `{ persist: true }`.
348
+
349
+ ### Observe progress on the parent
350
+
351
+ Override `onProgress()` to meter, steer, or surface progress server-side. It
352
+ fires best-effort whenever a child progress signal is forwarded through the
353
+ parent, for both awaited and detached runs:
354
+
355
+ ```ts
356
+ export class Assistant extends Think<Env> {
357
+ override async onProgress(
358
+ run: AgentToolRunInfo,
359
+ progress: AgentToolProgressSnapshot
360
+ ) {
361
+ if (progress.milestone) {
362
+ // A durable milestone landed — branch on it.
363
+ }
364
+ console.log(run.runId, progress.phase, progress.fraction);
365
+ }
366
+ }
367
+ ```
368
+
369
+ `onProgress()` is not durable: after eviction a detached run's latest snapshot is
370
+ reconstructed from `inspectAgentToolRun().progress` on reconcile rather than
371
+ re-firing the hook. The latest snapshot is also persisted on the child run row,
372
+ so a rehydrated parent can answer "where is this run" without having tailed the
373
+ live stream.
374
+
375
+ ### Durable milestones
376
+
377
+ Naming a `milestone` promotes a signal from the ephemeral tier to a **durable**
378
+ one — there is still only one emit method:
379
+
380
+ ```ts
381
+ await this.reportProgress({
382
+ milestone: "sources-gathered",
383
+ data: { sources: 2 }
384
+ });
385
+ ```
386
+
387
+ A milestone is persisted as one row on the child with a monotonic per-run
388
+ `sequence`, and rides the stream as a **persisted** `data-agent-milestone` part
389
+ (unlike transient progress). It therefore survives eviction, replays on drill-in,
390
+ and is surfaced — deduped by `sequence` — on `AgentToolRunState.milestones` and
391
+ `inspectAgentToolRun().milestones`. `onProgress()` fires for milestones too, with
392
+ `progress.milestone` set, so a consumer can branch on milestone versus ephemeral
393
+ progress.
394
+
395
+ ### Notify the chat on a milestone (Think / AIChatAgent)
396
+
397
+ For a detached run on a chat agent, `detached: { onMilestones }` surfaces a chat
398
+ message when a configured milestone lands, _before_ the run finishes. Each
399
+ `(runId, name)` fires at most once — whether observed live or reconciled after
400
+ eviction — so the deterministic id collapses warm and cold delivery to
401
+ at-most-once:
402
+
403
+ ```ts
404
+ // "narrate" (default): inject a synthetic assistant status line — no model turn.
405
+ await this.runAgentTool(Researcher, {
406
+ input,
407
+ detached: { onMilestones: ["sources-gathered"] }
408
+ });
409
+
410
+ // "react": post a user-role turn so the model responds (steer, start dependent
411
+ // work). Costs a model turn.
412
+ await this.runAgentTool(Researcher, {
413
+ input,
414
+ detached: { onMilestones: { names: ["needs-approval"], mode: "react" } }
415
+ });
416
+ ```
417
+
418
+ Override `formatDetachedMilestone(run, milestone)` to customize the wording, or
419
+ return an empty string to suppress a given milestone. Synthetic narrate messages
420
+ carry `metadata.source`, so clients can render them as an agent event rather than
421
+ a human turn.
422
+
423
+ ### Resetting no-progress budget for detached runs
424
+
425
+ Once a detached child has reported at least one signal, the reconcile backbone
426
+ gives up if the run then goes silent for `detachedNoProgressBudgetMs` (default 1
427
+ hour; per-run override via `detached: { noProgressBudgetMs }`). This surfaces as
428
+ `status: "interrupted"`, `reason: "no-progress"`. A child that never reports is
429
+ bounded only by the absolute `detachedMaxBudgetMs` ceiling — a run is never
430
+ given up on merely for being slow. Set `noProgressBudgetMs` to `0` or `Infinity`
431
+ to disable the resetting window for a run.
432
+
433
+ ## Render child timelines in React
434
+
435
+ `useAgentToolEvents()` is a headless hook. It subscribes to the existing parent
436
+ connection, deduplicates replay/live races, applies child `UIMessageChunk`
437
+ bodies to message parts, and groups sibling runs by parent tool call id. Each
438
+ run state carries `progress` and `milestones`, so a background-runs tray can
439
+ render a live bar, phase, and milestone chips without drilling in.
440
+
441
+ ```tsx
442
+ import { useAgent, useAgentToolEvents } from "agents/react";
443
+ import { useAgentChat } from "@cloudflare/ai-chat/react";
444
+
445
+ const agent = useAgent({ agent: "Assistant", name: userId });
446
+ const { messages } = useAgentChat({ agent });
447
+ const agentTools = useAgentToolEvents({ agent });
448
+
449
+ for (const message of messages) {
450
+ for (const part of message.parts) {
451
+ if (part.type === "tool-call") {
452
+ const runs = agentTools.getRunsForToolCall(part.toolCallId);
453
+ // Render the child runs beside this tool call.
454
+ }
455
+ }
456
+ }
457
+ ```
458
+
459
+ Imperative runs without a parent tool call are available as
460
+ `agentTools.unboundRuns`.
461
+
462
+ ## Drill in and gate access
463
+
464
+ Agent tools are normal sub-agents. Connect to a retained child through the
465
+ parent route:
466
+
467
+ ```ts
468
+ useAgent({
469
+ agent: "Assistant",
470
+ name: userId,
471
+ sub: [{ agent: "Researcher", name: runId }]
472
+ });
473
+ ```
474
+
475
+ Gate external access with the parent registry so guessed run ids cannot spawn
476
+ fresh child facets:
477
+
478
+ ```ts
479
+ override async onBeforeSubAgent(_request, child) {
480
+ if (!this.hasAgentToolRun(child.className, child.name)) {
481
+ return new Response("Not found", { status: 404 });
482
+ }
483
+ }
484
+ ```
485
+
486
+ ## Clear retained runs
487
+
488
+ Runs and child facets are retained by default for refresh, drill-in, and later
489
+ inspection. Delete them explicitly when clearing chat history or applying your
490
+ own retention policy:
491
+
492
+ ```ts
493
+ await this.clearAgentToolRuns();
494
+ await this.clearAgentToolRuns({
495
+ status: ["completed", "error", "aborted", "interrupted"]
496
+ });
497
+ await this.clearAgentToolRuns({ olderThan: Date.now() - 7 * 24 * 60 * 60_000 });
498
+ ```
499
+
500
+ If a retained run is still `starting` or `running`, cleanup cancels the child
501
+ before deleting its facet.
502
+
503
+ ## Interrupted runs and recovery
504
+
505
+ Agent-tool runs are retained in the parent. If the parent restarts (deploy,
506
+ eviction) while a child run is still `starting` or `running`, the parent does
507
+ not immediately abandon it. Startup recovery re-attaches to the live child and
508
+ tails its stream to the child's real terminal result: the child is a sub-agent
509
+ with its own `chatRecovery`, so it self-heals its interrupted turn while the
510
+ parent forwards its output and collects the final outcome. A completed child is
511
+ finalized in the parent without re-running already-finished work.
512
+
513
+ The re-attach wait is **progress-keyed**, not a fixed wall clock. Two static
514
+ `options` tune it:
515
+
516
+ | Option | Default | Behavior |
517
+ | -------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
518
+ | `agentToolReattachNoProgressTimeoutMs` | `120000` (2 min) | How long the parent waits with **no** forward progress before giving up. Resets on every forwarded chunk, so a streaming child is followed through to terminal. |
519
+ | `agentToolReattachMaxWindowMs` | `Infinity` | Optional hard wall-clock ceiling on a single re-attach. Uncapped by default (mirrors chat recovery's `maxRecoveryWork`), so a healthy, long-running child is never cut off. Set a finite value to impose a cap. |
520
+
521
+ Give-up outcomes map to the `AgentToolFailure` fields:
522
+
523
+ - A child that goes **silent** for a full no-progress window is sealed
524
+ `reason: "no-progress"`, `childStillRunning: true`. This seal is **soft** —
525
+ the child is left running, so re-dispatching the same `runId` can re-attach
526
+ and collect it if it self-heals.
527
+ - If you set a finite `agentToolReattachMaxWindowMs` and it fires, the run is
528
+ sealed `reason: "window-exceeded"`, `childStillRunning: false`, and the child
529
+ is **torn down** (it has had its full window and is treated as exhausted).
530
+ - A child that cannot be tailed or inspected, or that exceeds the overall
531
+ recovery deadline, is sealed with the matching `reason` so the parent tool
532
+ call returns a structured failure instead of hanging indefinitely.
533
+
534
+ A genuinely hung child can never block recovery forever: the no-progress budget
535
+ bounds a silent child, and a content-runaway is bounded uniformly (live and
536
+ recovery) by the child's own `chatRecovery` `maxRecoveryWork` /
537
+ `shouldKeepRecovering`, not by a parent-only timer.
538
+
539
+ Monitor parent reconciliation through the `agentTool` observability channel:
540
+
541
+ ```ts
542
+ import { subscribe } from "agents/observability";
543
+
544
+ const unsubscribe = subscribe("agentTool", (event) => {
545
+ if (event.type === "agent_tool:recovery:row") {
546
+ console.log("Recovered agent-tool row", event.payload);
547
+ }
548
+ });
549
+ ```
550
+
551
+ Raw `diagnostics_channel` subscribers should use the channel name
552
+ `agents:agent_tool`.