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
|
@@ -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`.
|