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,1506 @@
1
+ import { n as transition } from "../wire-types-nflOzNuU.js";
2
+ import { getToolName, isToolUIPart } from "ai";
3
+ import { nanoid } from "nanoid";
4
+ import { use, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { useChat } from "@ai-sdk/react";
6
+ //#region src/chat/ws-chat-transport.ts
7
+ /**
8
+ * Short safety-net timeout for a resume probe when the server has said nothing.
9
+ * Under normal operation the server answers a `STREAM_RESUME_REQUEST` with
10
+ * `STREAM_RESUMING`, `STREAM_RESUME_NONE`, or `STREAM_PENDING` well before this.
11
+ */
12
+ const RESUME_PROBE_TIMEOUT_MS = 5e3;
13
+ /**
14
+ * Extended backstop applied once the server says a turn is pending
15
+ * (`STREAM_PENDING`, #1784). The pre-stream window (queueing, MCP setup,
16
+ * debounce, model latency) can exceed the short probe timeout, and the server
17
+ * guarantees a follow-up `STREAM_RESUMING` or `STREAM_RESUME_NONE` — so we wait
18
+ * much longer (refreshed on every keep-waiting frame) but still cap it so a
19
+ * dropped follow-up degrades to a null resolve instead of hanging forever.
20
+ */
21
+ const RESUME_PENDING_TIMEOUT_MS = 6e4;
22
+ /**
23
+ * ChatTransport that sends messages over WebSocket and returns a
24
+ * ReadableStream<UIMessageChunk> that the AI SDK's useChat consumes directly.
25
+ * No fake fetch, no Response reconstruction, no double SSE parsing.
26
+ */
27
+ var WebSocketChatTransport = class {
28
+ constructor(options) {
29
+ this._resumeResolver = null;
30
+ this._resumeNoneResolver = null;
31
+ this._onStreamPending = null;
32
+ this._expectToolContinuation = false;
33
+ this._abortToolContinuation = null;
34
+ this._activeServerTurnId = null;
35
+ this._cancelAttachedStream = null;
36
+ this.agent = options.agent;
37
+ this.prepareBody = options.prepareBody;
38
+ this.activeRequestIds = options.activeRequestIds;
39
+ this.cancelOnClientAbort = options.cancelOnClientAbort ?? false;
40
+ }
41
+ setCancelOnClientAbort(cancelOnClientAbort) {
42
+ this.cancelOnClientAbort = cancelOnClientAbort;
43
+ }
44
+ /**
45
+ * Explicitly cancel the active server turn, if any.
46
+ * This is separate from generic client-side abort/cancel lifecycle so
47
+ * clients can detach locally without stopping server work.
48
+ */
49
+ cancelActiveServerTurn() {
50
+ const requestId = this._activeServerTurnId;
51
+ let cancelledRequest = false;
52
+ if (requestId) {
53
+ this.sendCancelFrame(requestId);
54
+ this._cancelAttachedStream?.();
55
+ this.clearActiveServerTurn(requestId);
56
+ cancelledRequest = true;
57
+ }
58
+ const cancelledToolContinuation = this.abortActiveToolContinuation();
59
+ return cancelledRequest || cancelledToolContinuation;
60
+ }
61
+ sendCancelFrame(requestId) {
62
+ try {
63
+ this.agent.send(JSON.stringify({
64
+ id: requestId,
65
+ type: "cf_agent_chat_request_cancel"
66
+ }));
67
+ } catch {}
68
+ }
69
+ setActiveServerTurn(requestId, cancelAttachedStream) {
70
+ this._activeServerTurnId = requestId;
71
+ this._cancelAttachedStream = cancelAttachedStream;
72
+ }
73
+ clearActiveServerTurn(requestId) {
74
+ if (this._activeServerTurnId === requestId) {
75
+ this._activeServerTurnId = null;
76
+ this._cancelAttachedStream = null;
77
+ }
78
+ }
79
+ /**
80
+ * Mark that the next reconnectToStream() call should attach to a
81
+ * server-initiated tool continuation rather than a page-load resume.
82
+ */
83
+ expectToolContinuation() {
84
+ this._expectToolContinuation = true;
85
+ }
86
+ /**
87
+ * Abort the active client-side tool continuation stream, if one is attached
88
+ * to a server request id.
89
+ */
90
+ abortActiveToolContinuation() {
91
+ return this._abortToolContinuation?.() ?? false;
92
+ }
93
+ /**
94
+ * True when the transport is waiting for a resume handshake.
95
+ */
96
+ isAwaitingResume() {
97
+ return this._resumeResolver !== null || this._resumeNoneResolver !== null;
98
+ }
99
+ /**
100
+ * Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUMING.
101
+ * If reconnectToStream is waiting, this handles the resume handshake
102
+ * (ACK + stream creation) and returns true. Otherwise returns false
103
+ * so the caller can use its own fallback path.
104
+ */
105
+ handleStreamResuming(data) {
106
+ if (!this._resumeResolver) return false;
107
+ this._resumeResolver(data);
108
+ return true;
109
+ }
110
+ /**
111
+ * Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUME_NONE.
112
+ * If reconnectToStream is waiting, resolves the promise with null
113
+ * immediately (no 5-second timeout). Returns true if handled.
114
+ */
115
+ handleStreamResumeNone() {
116
+ if (!this._resumeNoneResolver) return false;
117
+ this._resumeNoneResolver();
118
+ return true;
119
+ }
120
+ /**
121
+ * Called by onAgentMessage when it receives CF_AGENT_STREAM_PENDING (#1784):
122
+ * the server accepted a turn but its stream has not started yet. If a resume
123
+ * path is awaiting, extend its probe timeout (so it keeps waiting for the
124
+ * eventual STREAM_RESUMING / STREAM_RESUME_NONE instead of resolving null
125
+ * after the short window). Returns true if a waiting path consumed it.
126
+ */
127
+ handleStreamPending() {
128
+ if (!this._onStreamPending) return false;
129
+ this._onStreamPending();
130
+ return true;
131
+ }
132
+ /**
133
+ * Called by the hook's shared message handler when a server turn finishes
134
+ * outside the currently attached transport stream, such as after local-only
135
+ * client cleanup.
136
+ */
137
+ handleServerTurnCompleted(requestId) {
138
+ this.clearActiveServerTurn(requestId);
139
+ }
140
+ /**
141
+ * Register a server turn that is being rendered outside a transport-owned
142
+ * stream, such as the hook's fallback cross-tab/resume observer path.
143
+ */
144
+ observeServerTurn(requestId) {
145
+ this.setActiveServerTurn(requestId, null);
146
+ }
147
+ async sendMessages(options) {
148
+ const requestId = nanoid(8);
149
+ const abortController = new AbortController();
150
+ let completed = false;
151
+ let requestSent = false;
152
+ let extraBody = {};
153
+ if (this.prepareBody) extraBody = await this.prepareBody({
154
+ messages: options.messages,
155
+ trigger: options.trigger,
156
+ messageId: options.messageId
157
+ });
158
+ if (options.body) extraBody = {
159
+ ...extraBody,
160
+ ...options.body
161
+ };
162
+ const bodyPayload = JSON.stringify({
163
+ messages: options.messages,
164
+ trigger: options.trigger,
165
+ ...extraBody
166
+ });
167
+ this.activeRequestIds?.add(requestId);
168
+ const agent = this.agent;
169
+ const activeIds = this.activeRequestIds;
170
+ const finish = (action, keepId = false, clearServerTurn = true) => {
171
+ if (completed) return;
172
+ completed = true;
173
+ if (clearServerTurn) this.clearActiveServerTurn(requestId);
174
+ try {
175
+ action();
176
+ } catch {}
177
+ if (!keepId) activeIds?.delete(requestId);
178
+ abortController.abort();
179
+ };
180
+ const abortError = /* @__PURE__ */ new Error("Aborted");
181
+ abortError.name = "AbortError";
182
+ const cancelActiveRequest = () => {
183
+ if (completed) return false;
184
+ finish(() => streamController.error(abortError), true);
185
+ return true;
186
+ };
187
+ this.setActiveServerTurn(requestId, cancelActiveRequest);
188
+ const onAbort = () => {
189
+ if (completed) return;
190
+ if (this.cancelOnClientAbort) {
191
+ if (requestSent) this.sendCancelFrame(requestId);
192
+ finish(() => streamController.error(abortError), requestSent);
193
+ } else finish(() => streamController.error(abortError), false, !requestSent);
194
+ };
195
+ let streamController;
196
+ const stream = new ReadableStream({
197
+ start(controller) {
198
+ streamController = controller;
199
+ const onMessage = (event) => {
200
+ try {
201
+ const data = JSON.parse(event.data);
202
+ if (data.type !== "cf_agent_use_chat_response") return;
203
+ if (data.id !== requestId) return;
204
+ if (data.error) {
205
+ finish(() => controller.error(new Error(data.body || "Stream error")));
206
+ return;
207
+ }
208
+ if (data.body?.trim()) try {
209
+ const chunk = JSON.parse(data.body);
210
+ controller.enqueue(chunk);
211
+ } catch {}
212
+ if (data.done) finish(() => controller.close());
213
+ } catch {}
214
+ };
215
+ const onClose = () => {
216
+ finish(() => controller.close(), false, false);
217
+ };
218
+ agent.addEventListener("message", onMessage, { signal: abortController.signal });
219
+ agent.addEventListener("close", onClose, { signal: abortController.signal });
220
+ },
221
+ cancel() {
222
+ onAbort();
223
+ }
224
+ });
225
+ if (options.abortSignal) {
226
+ options.abortSignal.addEventListener("abort", onAbort, { once: true });
227
+ if (options.abortSignal.aborted) onAbort();
228
+ }
229
+ if (completed) return stream;
230
+ requestSent = true;
231
+ agent.send(JSON.stringify({
232
+ id: requestId,
233
+ init: {
234
+ method: "POST",
235
+ body: bodyPayload
236
+ },
237
+ type: "cf_agent_use_chat_request"
238
+ }));
239
+ return stream;
240
+ }
241
+ async reconnectToStream(_options) {
242
+ if (this._expectToolContinuation) {
243
+ this._expectToolContinuation = false;
244
+ return this._createToolContinuationStream();
245
+ }
246
+ const activeIds = this.activeRequestIds;
247
+ return new Promise((resolve) => {
248
+ let resolved = false;
249
+ let timeout;
250
+ const done = (value) => {
251
+ if (resolved) return;
252
+ resolved = true;
253
+ this._resumeResolver = null;
254
+ this._resumeNoneResolver = null;
255
+ this._onStreamPending = null;
256
+ if (timeout) clearTimeout(timeout);
257
+ resolve(value);
258
+ };
259
+ this._onStreamPending = () => {
260
+ if (resolved) return;
261
+ if (timeout) clearTimeout(timeout);
262
+ timeout = setTimeout(() => done(null), RESUME_PENDING_TIMEOUT_MS);
263
+ };
264
+ this._resumeNoneResolver = () => done(null);
265
+ this._resumeResolver = (data) => {
266
+ const requestId = data.id;
267
+ activeIds?.add(requestId);
268
+ const stream = this._createResumeStream(requestId);
269
+ this.agent.send(JSON.stringify({
270
+ type: "cf_agent_stream_resume_ack",
271
+ id: requestId
272
+ }));
273
+ done(stream);
274
+ };
275
+ try {
276
+ this.agent.send(JSON.stringify({ type: "cf_agent_stream_resume_request" }));
277
+ } catch {}
278
+ timeout = setTimeout(() => done(null), RESUME_PROBE_TIMEOUT_MS);
279
+ });
280
+ }
281
+ /**
282
+ * Creates a deferred ReadableStream for client-side tool continuations.
283
+ * The stream is returned immediately so AI SDK status becomes "submitted"
284
+ * right after addToolOutput()/addToolApprovalResponse(), then it waits for
285
+ * the server to announce the continuation via STREAM_RESUMING.
286
+ */
287
+ _createToolContinuationStream() {
288
+ const agent = this.agent;
289
+ const activeIds = this.activeRequestIds;
290
+ const streamController = new AbortController();
291
+ const abortError = /* @__PURE__ */ new Error("Aborted");
292
+ abortError.name = "AbortError";
293
+ let completed = false;
294
+ let requestId = null;
295
+ let readerController;
296
+ let onResumeRef = null;
297
+ let onResumeNoneRef = null;
298
+ const clearHandshakeResolvers = (resumeResolver, resumeNoneResolver) => {
299
+ if (resumeResolver === void 0 && resumeNoneResolver === void 0) {
300
+ this._resumeResolver = null;
301
+ this._resumeNoneResolver = null;
302
+ return;
303
+ }
304
+ if (resumeResolver && this._resumeResolver === resumeResolver) this._resumeResolver = null;
305
+ if (resumeNoneResolver && this._resumeNoneResolver === resumeNoneResolver) this._resumeNoneResolver = null;
306
+ };
307
+ const finish = (action, resumeResolver, resumeNoneResolver, keepRequestId = false) => {
308
+ if (completed) return;
309
+ completed = true;
310
+ this._abortToolContinuation = null;
311
+ this._onStreamPending = null;
312
+ clearHandshakeResolvers(resumeResolver, resumeNoneResolver);
313
+ try {
314
+ action();
315
+ } catch {}
316
+ if (requestId && !keepRequestId) activeIds?.delete(requestId);
317
+ streamController.abort();
318
+ };
319
+ const transport = this;
320
+ this._abortToolContinuation = () => {
321
+ if (completed) return false;
322
+ if (requestId === null) {
323
+ finish(() => readerController.error(abortError), onResumeRef, onResumeNoneRef);
324
+ return true;
325
+ }
326
+ try {
327
+ agent.send(JSON.stringify({
328
+ type: "cf_agent_chat_request_cancel",
329
+ id: requestId
330
+ }));
331
+ } catch {}
332
+ finish(() => readerController.error(abortError), onResumeRef, onResumeNoneRef, true);
333
+ return true;
334
+ };
335
+ return new ReadableStream({
336
+ start(controller) {
337
+ readerController = controller;
338
+ let timeout;
339
+ const onResumeNone = () => {
340
+ if (timeout) clearTimeout(timeout);
341
+ finish(() => controller.close(), onResume, onResumeNone);
342
+ };
343
+ const onResume = (data) => {
344
+ if (requestId) return;
345
+ requestId = data.id;
346
+ activeIds?.add(requestId);
347
+ clearHandshakeResolvers(onResume, onResumeNone);
348
+ transport._onStreamPending = null;
349
+ if (timeout) clearTimeout(timeout);
350
+ agent.send(JSON.stringify({
351
+ type: "cf_agent_stream_resume_ack",
352
+ id: requestId
353
+ }));
354
+ };
355
+ onResumeRef = onResume;
356
+ onResumeNoneRef = onResumeNone;
357
+ timeout = setTimeout(() => finish(() => controller.close(), onResume, onResumeNone), RESUME_PROBE_TIMEOUT_MS);
358
+ transport._onStreamPending = () => {
359
+ if (completed) return;
360
+ if (timeout) clearTimeout(timeout);
361
+ timeout = setTimeout(() => finish(() => controller.close(), onResume, onResumeNone), RESUME_PENDING_TIMEOUT_MS);
362
+ };
363
+ transport._resumeResolver = onResume;
364
+ transport._resumeNoneResolver = onResumeNone;
365
+ const onMessage = (event) => {
366
+ try {
367
+ const data = JSON.parse(event.data);
368
+ if (data.type !== "cf_agent_use_chat_response" || requestId == null || data.id !== requestId) return;
369
+ if (data.error) {
370
+ finish(() => controller.error(new Error(data.body || "Stream error")), onResume, onResumeNone);
371
+ return;
372
+ }
373
+ if (data.body?.trim()) try {
374
+ const chunk = JSON.parse(data.body);
375
+ controller.enqueue(chunk);
376
+ } catch {}
377
+ if (data.done) finish(() => controller.close(), onResume, onResumeNone);
378
+ } catch {}
379
+ };
380
+ const onClose = () => {
381
+ if (timeout) clearTimeout(timeout);
382
+ finish(() => controller.close(), onResume, onResumeNone);
383
+ };
384
+ agent.addEventListener("message", onMessage, { signal: streamController.signal });
385
+ agent.addEventListener("close", onClose, { signal: streamController.signal });
386
+ try {
387
+ agent.send(JSON.stringify({ type: "cf_agent_stream_resume_request" }));
388
+ } catch {
389
+ finish(() => controller.close());
390
+ }
391
+ },
392
+ cancel() {
393
+ if (requestId && transport.cancelOnClientAbort) {
394
+ transport.sendCancelFrame(requestId);
395
+ finish(() => {}, onResumeRef, onResumeNoneRef, true);
396
+ } else finish(() => {}, onResumeRef, onResumeNoneRef);
397
+ }
398
+ });
399
+ }
400
+ /**
401
+ * Creates a ReadableStream that receives resumed stream chunks
402
+ * and forwards them to useChat as UIMessageChunk objects.
403
+ */
404
+ _createResumeStream(requestId) {
405
+ const agent = this.agent;
406
+ const activeIds = this.activeRequestIds;
407
+ const chunkController = new AbortController();
408
+ const abortError = /* @__PURE__ */ new Error("Aborted");
409
+ abortError.name = "AbortError";
410
+ let completed = false;
411
+ const finish = (action, keepId = false, clearServerTurn = true) => {
412
+ if (completed) return;
413
+ completed = true;
414
+ if (clearServerTurn) this.clearActiveServerTurn(requestId);
415
+ try {
416
+ action();
417
+ } catch {}
418
+ if (!keepId) activeIds?.delete(requestId);
419
+ chunkController.abort();
420
+ };
421
+ const cancelActiveRequest = () => {
422
+ if (completed) return false;
423
+ finish(() => streamController.error(abortError), true);
424
+ return true;
425
+ };
426
+ this.setActiveServerTurn(requestId, cancelActiveRequest);
427
+ let streamController;
428
+ const transport = this;
429
+ return new ReadableStream({
430
+ start(controller) {
431
+ streamController = controller;
432
+ const onMessage = (event) => {
433
+ try {
434
+ const data = JSON.parse(event.data);
435
+ if (data.type !== "cf_agent_use_chat_response") return;
436
+ if (data.id !== requestId) return;
437
+ if (data.error) {
438
+ finish(() => controller.error(new Error(data.body || "Stream error")));
439
+ return;
440
+ }
441
+ if (data.body?.trim()) try {
442
+ const chunk = JSON.parse(data.body);
443
+ controller.enqueue(chunk);
444
+ } catch {}
445
+ if (data.done) finish(() => controller.close());
446
+ } catch {}
447
+ };
448
+ const onClose = () => {
449
+ finish(() => controller.close(), false, false);
450
+ };
451
+ agent.addEventListener("message", onMessage, { signal: chunkController.signal });
452
+ agent.addEventListener("close", onClose, { signal: chunkController.signal });
453
+ },
454
+ cancel() {
455
+ if (transport.cancelOnClientAbort) {
456
+ transport.sendCancelFrame(requestId);
457
+ finish(() => {}, true);
458
+ } else finish(() => {}, false, false);
459
+ }
460
+ });
461
+ }
462
+ };
463
+ //#endregion
464
+ //#region src/chat/react.tsx
465
+ /**
466
+ * One-shot deprecation warnings (warns once per key per session).
467
+ */
468
+ const _deprecationWarnings = /* @__PURE__ */ new Set();
469
+ function warnDeprecated(id, message) {
470
+ if (!_deprecationWarnings.has(id)) {
471
+ _deprecationWarnings.add(id);
472
+ console.warn(`[agents/chat] Deprecated: ${message}`);
473
+ }
474
+ }
475
+ /**
476
+ * Extracts tool schemas from tools that have client-side execute functions.
477
+ * These schemas are automatically sent to the server with each request.
478
+ *
479
+ * Called internally by `useAgentChat` when `tools` are provided.
480
+ * Most apps do not need to call this directly.
481
+ *
482
+ * @param tools - Record of tool name to tool definition
483
+ * @returns Array of tool schemas to send to server, or undefined if none
484
+ */
485
+ function extractClientToolSchemas(tools) {
486
+ if (!tools) return void 0;
487
+ const schemas = Object.entries(tools).filter(([_, tool]) => tool.execute).map(([name, tool]) => {
488
+ if (tool.inputSchema && !tool.parameters) console.warn(`[useAgentChat] Tool "${name}" uses deprecated 'inputSchema'. Please migrate to 'parameters'.`);
489
+ return {
490
+ name,
491
+ description: tool.description,
492
+ parameters: tool.parameters ?? tool.inputSchema
493
+ };
494
+ });
495
+ return schemas.length > 0 ? schemas : void 0;
496
+ }
497
+ /**
498
+ * Map internal tool part states to simplified UI-relevant states.
499
+ *
500
+ * @example
501
+ * ```tsx
502
+ * import { isToolUIPart } from "ai";
503
+ * import { getToolPartState } from "@cloudflare/ai-chat/react";
504
+ *
505
+ * if (isToolUIPart(part)) {
506
+ * const state = getToolPartState(part);
507
+ * if (state === "complete") { ... }
508
+ * if (state === "waiting-approval") { ... }
509
+ * }
510
+ * ```
511
+ */
512
+ function getToolPartState(part) {
513
+ switch (part.state) {
514
+ case "input-streaming": return "streaming";
515
+ case "approval-requested": return "waiting-approval";
516
+ case "approval-responded": return "approved";
517
+ case "output-available": return "complete";
518
+ case "output-error": return "error";
519
+ case "output-denied": return "denied";
520
+ default: return "loading";
521
+ }
522
+ }
523
+ /** Get the tool call ID from a tool UI part. */
524
+ function getToolCallId(part) {
525
+ return part.toolCallId;
526
+ }
527
+ /** Get the tool input from a tool UI part (if available). */
528
+ function getToolInput(part) {
529
+ return part.input;
530
+ }
531
+ /** Get the tool output from a tool UI part (if available). */
532
+ function getToolOutput(part) {
533
+ return part.output;
534
+ }
535
+ /** Get the approval info from a tool UI part (if in approval state). */
536
+ function getToolApproval(part) {
537
+ return part.approval;
538
+ }
539
+ function agentNameToKebab(name) {
540
+ if (name === name.toUpperCase() && name !== name.toLowerCase()) return name.toLowerCase().replace(/_/g, "-");
541
+ let result = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
542
+ result = result.startsWith("-") ? result.slice(1) : result;
543
+ return result.replace(/_/g, "-").replace(/-$/, "");
544
+ }
545
+ /**
546
+ * Fetch messages from an agent's `/get-messages` HTTP endpoint.
547
+ *
548
+ * Use in framework route loaders to prefetch messages before the component
549
+ * tree mounts, or anywhere you need messages outside a React hook.
550
+ *
551
+ * @example Standard routing
552
+ * ```typescript
553
+ * import { getAgentMessages } from "@cloudflare/ai-chat/react";
554
+ *
555
+ * const messages = await getAgentMessages({
556
+ * host: "https://my-app.workers.dev",
557
+ * agent: "ChatAgent",
558
+ * name: "session-123"
559
+ * });
560
+ * ```
561
+ *
562
+ * @example With basePath (custom URL)
563
+ * ```typescript
564
+ * const messages = await getAgentMessages({
565
+ * url: "https://my-app.workers.dev/custom/path/get-messages"
566
+ * });
567
+ * ```
568
+ */
569
+ async function getAgentMessages(options) {
570
+ let messagesUrl;
571
+ if ("url" in options) messagesUrl = options.url;
572
+ else {
573
+ const agentSlug = agentNameToKebab(options.agent);
574
+ messagesUrl = `${options.host.endsWith("/") ? options.host.slice(0, -1) : options.host}/agents/${agentSlug}/${options.name}/get-messages`;
575
+ }
576
+ try {
577
+ const response = await fetch(messagesUrl, {
578
+ credentials: options.credentials,
579
+ headers: options.headers
580
+ });
581
+ if (!response.ok) {
582
+ console.warn(`[getAgentMessages] Failed to fetch: ${response.status} ${response.statusText}`);
583
+ return [];
584
+ }
585
+ const text = await response.text();
586
+ if (!text.trim()) return [];
587
+ return JSON.parse(text);
588
+ } catch (error) {
589
+ console.warn("[getAgentMessages] Fetch error:", error);
590
+ return [];
591
+ }
592
+ }
593
+ /**
594
+ * Module-level cache for initial message fetches. Intentionally shared across
595
+ * all useAgentChat instances to deduplicate requests during React Strict Mode
596
+ * double-renders and re-renders. Cache keys include the agent URL, agent type,
597
+ * and thread name to prevent cross-agent collisions.
598
+ */
599
+ const requestCache = /* @__PURE__ */ new Map();
600
+ function findLastAssistantMessage(messages) {
601
+ for (let index = messages.length - 1; index >= 0; index--) {
602
+ const message = messages[index];
603
+ if (message.role === "assistant") return {
604
+ index,
605
+ message
606
+ };
607
+ }
608
+ return null;
609
+ }
610
+ function moveMessageToEnd(messages, messageId) {
611
+ const idx = messages.findIndex((m) => m.id === messageId);
612
+ if (idx < 0 || idx === messages.length - 1) return messages;
613
+ const result = [...messages];
614
+ const [msg] = result.splice(idx, 1);
615
+ if (!msg) return messages;
616
+ result.push(msg);
617
+ return result;
618
+ }
619
+ function prependMissingHydratedMessages(hydratedMessages, currentMessages) {
620
+ if (currentMessages.length === 0) return hydratedMessages;
621
+ const currentMessageIds = new Set(currentMessages.map((message) => message.id));
622
+ const missingHydratedMessages = hydratedMessages.filter((message) => !currentMessageIds.has(message.id));
623
+ if (missingHydratedMessages.length === 0) return currentMessages;
624
+ return [...missingHydratedMessages, ...currentMessages];
625
+ }
626
+ /**
627
+ * React hook for building AI chat interfaces using an Agent
628
+ * @param options Chat options including the agent connection
629
+ * @returns Chat interface controls and state with added clearHistory method
630
+ */
631
+ /**
632
+ * Automatically detects which tools require confirmation based on their configuration.
633
+ * Tools require confirmation if they have no execute function AND are not server-executed.
634
+ * @param tools - Record of tool name to tool definition
635
+ * @returns Array of tool names that require confirmation
636
+ *
637
+ * @deprecated Use `needsApproval` on server-side tools instead.
638
+ */
639
+ function detectToolsRequiringConfirmation(tools) {
640
+ warnDeprecated("detectToolsRequiringConfirmation", "detectToolsRequiringConfirmation() is deprecated. Use needsApproval on server-side tools instead. Will be removed in the next major version.");
641
+ if (!tools) return [];
642
+ return Object.entries(tools).filter(([_name, tool]) => !tool.execute).map(([name]) => name);
643
+ }
644
+ function useAgentChat(options) {
645
+ const { agent, getInitialMessages, messages: optionsInitialMessages, onToolCall, onData, experimental_automaticToolResolution, tools, toolsRequiringConfirmation: manualToolsRequiringConfirmation, autoContinueAfterToolResult = true, autoSendAfterAllConfirmationsResolved = true, resume = true, cancelOnClientAbort = false, syncMessagesToServer = true, body: bodyOption, prepareSendMessagesRequest, ...rest } = options;
646
+ if (manualToolsRequiringConfirmation) warnDeprecated("useAgentChat.toolsRequiringConfirmation", "The 'toolsRequiringConfirmation' option is deprecated. Use needsApproval on server-side tools instead. Will be removed in the next major version.");
647
+ if (experimental_automaticToolResolution) warnDeprecated("useAgentChat.experimental_automaticToolResolution", "The 'experimental_automaticToolResolution' option is deprecated. Use the onToolCall callback instead. Will be removed in the next major version.");
648
+ if (options.autoSendAfterAllConfirmationsResolved !== void 0) warnDeprecated("useAgentChat.autoSendAfterAllConfirmationsResolved", "The 'autoSendAfterAllConfirmationsResolved' option is deprecated. Use sendAutomaticallyWhen from AI SDK instead. Will be removed in the next major version.");
649
+ const toolsRequiringConfirmation = useMemo(() => {
650
+ if (manualToolsRequiringConfirmation) return manualToolsRequiringConfirmation;
651
+ if (!tools) return [];
652
+ return Object.entries(tools).filter(([_name, tool]) => !tool.execute).map(([name]) => name);
653
+ }, [manualToolsRequiringConfirmation, tools]);
654
+ const onToolCallRef = useRef(onToolCall);
655
+ onToolCallRef.current = onToolCall;
656
+ const onDataRef = useRef(onData);
657
+ onDataRef.current = onData;
658
+ const rawHttpUrl = agent.getHttpUrl();
659
+ const agentUrl = rawHttpUrl ? new URL(rawHttpUrl) : null;
660
+ if (agentUrl) agentUrl.searchParams.delete("_pk");
661
+ const agentUrlString = agentUrl?.toString() ?? null;
662
+ const agentAddressKey = Array.isArray(agent.path) ? JSON.stringify(agent.path.map((step) => [step.agent, step.name])) : JSON.stringify([[agent.agent ?? "", agent.name ?? ""]]);
663
+ const resolvedInitialMessagesCacheKey = agentUrl ? `${agentUrl.origin}${agentUrl.pathname}|${agentAddressKey}` : null;
664
+ const initialMessagesCacheKey = agentAddressKey;
665
+ const stableChatIdRef = useRef(null);
666
+ const previousAgentRef = useRef(null);
667
+ const previousAgentAddressKeyRef = useRef(null);
668
+ const fallbackChatId = agentAddressKey;
669
+ const agentPathChanged = Array.isArray(agent.path) && previousAgentAddressKeyRef.current !== null && previousAgentAddressKeyRef.current !== agentAddressKey;
670
+ if (stableChatIdRef.current === null) stableChatIdRef.current = resolvedInitialMessagesCacheKey ?? fallbackChatId;
671
+ else if (previousAgentRef.current !== agent || agentPathChanged) stableChatIdRef.current = resolvedInitialMessagesCacheKey ?? fallbackChatId;
672
+ previousAgentRef.current = agent;
673
+ previousAgentAddressKeyRef.current = agentAddressKey;
674
+ const agentRef = useRef(agent);
675
+ agentRef.current = agent;
676
+ async function defaultGetInitialMessagesFetch({ url }) {
677
+ if (!url) return [];
678
+ const getMessagesUrl = new URL(url);
679
+ getMessagesUrl.pathname += "/get-messages";
680
+ const response = await fetch(getMessagesUrl.toString(), {
681
+ credentials: options.credentials,
682
+ headers: options.headers
683
+ });
684
+ if (!response.ok) {
685
+ console.warn(`Failed to fetch initial messages: ${response.status} ${response.statusText}`);
686
+ return [];
687
+ }
688
+ const text = await response.text();
689
+ if (!text.trim()) return [];
690
+ try {
691
+ return JSON.parse(text);
692
+ } catch (error) {
693
+ console.warn("Failed to parse initial messages JSON:", error);
694
+ return [];
695
+ }
696
+ }
697
+ const getInitialMessagesFetch = getInitialMessages || defaultGetInitialMessagesFetch;
698
+ function doGetInitialMessages(getInitialMessagesOptions, cacheKey) {
699
+ if (requestCache.has(cacheKey)) return requestCache.get(cacheKey);
700
+ const promise = getInitialMessagesFetch(getInitialMessagesOptions);
701
+ requestCache.set(cacheKey, promise);
702
+ return promise;
703
+ }
704
+ const initialMessagesPromise = !(getInitialMessages === null ? false : getInitialMessages ? true : !!agentUrlString) ? null : doGetInitialMessages({
705
+ agent: agent.agent,
706
+ name: agent.name,
707
+ url: agentUrlString ?? void 0
708
+ }, initialMessagesCacheKey);
709
+ const initialMessages = initialMessagesPromise ? use(initialMessagesPromise) : optionsInitialMessages ?? [];
710
+ useEffect(() => {
711
+ if (!initialMessagesPromise) return;
712
+ requestCache.set(initialMessagesCacheKey, initialMessagesPromise);
713
+ return () => {
714
+ if (requestCache.get(initialMessagesCacheKey) === initialMessagesPromise) requestCache.delete(initialMessagesCacheKey);
715
+ };
716
+ }, [initialMessagesCacheKey, initialMessagesPromise]);
717
+ const toolsRef = useRef(tools);
718
+ toolsRef.current = tools;
719
+ const prepareSendMessagesRequestRef = useRef(prepareSendMessagesRequest);
720
+ prepareSendMessagesRequestRef.current = prepareSendMessagesRequest;
721
+ const bodyOptionRef = useRef(bodyOption);
722
+ bodyOptionRef.current = bodyOption;
723
+ /**
724
+ * Tracks request IDs initiated by this tab via the transport.
725
+ * Used by onAgentMessage to skip messages already handled by the transport.
726
+ */
727
+ const localRequestIdsRef = useRef(/* @__PURE__ */ new Set());
728
+ const pendingReplayResumeRequestIdsRef = useRef(/* @__PURE__ */ new Set());
729
+ const replayHydratedAssistantMessageIdsRef = useRef(/* @__PURE__ */ new Set());
730
+ /**
731
+ * Request ids this socket already ACKed via the fallback resume path.
732
+ * The server sends CF_AGENT_STREAM_RESUMING for the same request from
733
+ * both onConnect and its CF_AGENT_STREAM_RESUME_REQUEST handler (#1733).
734
+ * The transport-handled path dedupes the second notify via
735
+ * localRequestIdsRef, but the fallback path used to ACK both — triggering
736
+ * a second full-buffer replay that duplicated streamed parts. Entries are
737
+ * dropped when the turn completes; the whole set resets when the socket
738
+ * closes, since a new connection legitimately needs a fresh ACK+replay.
739
+ */
740
+ const fallbackAckedResumeRequestIdsRef = useRef(/* @__PURE__ */ new Set());
741
+ const customTransportRef = useRef(null);
742
+ if (customTransportRef.current === null) customTransportRef.current = new WebSocketChatTransport({
743
+ agent: agentRef.current,
744
+ activeRequestIds: localRequestIdsRef.current,
745
+ cancelOnClientAbort,
746
+ prepareBody: async ({ messages: msgs, trigger, messageId }) => {
747
+ let extraBody = {};
748
+ const currentBody = bodyOptionRef.current;
749
+ if (currentBody) extraBody = { ...typeof currentBody === "function" ? await currentBody() : currentBody };
750
+ if (toolsRef.current) {
751
+ const clientToolSchemas = extractClientToolSchemas(toolsRef.current);
752
+ if (clientToolSchemas) extraBody.clientTools = clientToolSchemas;
753
+ }
754
+ if (prepareSendMessagesRequestRef.current) {
755
+ const userResult = await prepareSendMessagesRequestRef.current({
756
+ id: agentRef.current._pk,
757
+ messages: msgs,
758
+ trigger,
759
+ messageId
760
+ });
761
+ if (userResult.body) Object.assign(extraBody, userResult.body);
762
+ }
763
+ return extraBody;
764
+ }
765
+ });
766
+ customTransportRef.current.agent = agentRef.current;
767
+ customTransportRef.current.setCancelOnClientAbort(cancelOnClientAbort);
768
+ const customTransport = customTransportRef.current;
769
+ const useChatHelpers = useChat({
770
+ ...rest,
771
+ onData,
772
+ messages: initialMessages,
773
+ transport: customTransport,
774
+ id: stableChatIdRef.current,
775
+ resume
776
+ });
777
+ const { messages: chatMessages, setMessages, addToolResult, addToolApprovalResponse, sendMessage, resumeStream, status, stop } = useChatHelpers;
778
+ const statusRef = useRef(status);
779
+ statusRef.current = status;
780
+ const resumeStreamRef = useRef(resumeStream);
781
+ resumeStreamRef.current = resumeStream;
782
+ const hasConnectedOnceRef = useRef(false);
783
+ const resumingToolContinuationRef = useRef(false);
784
+ const pendingToolContinuationRef = useRef(false);
785
+ const observedToolContinuationRequestIdRef = useRef(null);
786
+ const continuationLaunchTimerRef = useRef(null);
787
+ const continuationGenerationRef = useRef(0);
788
+ const [isToolContinuation, setIsToolContinuation] = useState(false);
789
+ const resetToolContinuation = useCallback(() => {
790
+ continuationGenerationRef.current++;
791
+ pendingToolContinuationRef.current = false;
792
+ resumingToolContinuationRef.current = false;
793
+ observedToolContinuationRequestIdRef.current = null;
794
+ if (continuationLaunchTimerRef.current) {
795
+ clearTimeout(continuationLaunchTimerRef.current);
796
+ continuationLaunchTimerRef.current = null;
797
+ }
798
+ setIsToolContinuation(false);
799
+ }, []);
800
+ const scheduleToolContinuationLaunch = useCallback(() => {
801
+ if (!pendingToolContinuationRef.current || statusRef.current !== "ready" || continuationLaunchTimerRef.current) return;
802
+ continuationLaunchTimerRef.current = setTimeout(() => {
803
+ continuationLaunchTimerRef.current = null;
804
+ if (!pendingToolContinuationRef.current || statusRef.current !== "ready") return;
805
+ pendingToolContinuationRef.current = false;
806
+ const myGeneration = continuationGenerationRef.current;
807
+ customTransport.expectToolContinuation();
808
+ resumeStream().catch((error) => {
809
+ console.error("[useAgentChat] Tool continuation resume failed:", error);
810
+ }).finally(() => {
811
+ if (continuationGenerationRef.current !== myGeneration) return;
812
+ resumingToolContinuationRef.current = false;
813
+ setIsToolContinuation(false);
814
+ });
815
+ }, 0);
816
+ }, [customTransport, resumeStream]);
817
+ const startToolContinuation = useCallback(() => {
818
+ if (!autoContinueAfterToolResult || resumingToolContinuationRef.current) return;
819
+ ++continuationGenerationRef.current;
820
+ resumingToolContinuationRef.current = true;
821
+ pendingToolContinuationRef.current = true;
822
+ setIsToolContinuation(true);
823
+ scheduleToolContinuationLaunch();
824
+ }, [autoContinueAfterToolResult, scheduleToolContinuationLaunch]);
825
+ useEffect(() => {
826
+ if (status === "error" && pendingToolContinuationRef.current) {
827
+ resetToolContinuation();
828
+ return;
829
+ }
830
+ scheduleToolContinuationLaunch();
831
+ }, [
832
+ resetToolContinuation,
833
+ scheduleToolContinuationLaunch,
834
+ status
835
+ ]);
836
+ const stopWithToolContinuationAbort = useCallback(async () => {
837
+ try {
838
+ customTransport.cancelActiveServerTurn();
839
+ await stop();
840
+ } finally {
841
+ customTransport.abortActiveToolContinuation();
842
+ }
843
+ }, [stop, customTransport]);
844
+ const processedToolCalls = useRef(/* @__PURE__ */ new Set());
845
+ const isResolvingToolsRef = useRef(false);
846
+ const [toolResolutionTrigger, setToolResolutionTrigger] = useState(0);
847
+ const [clientToolResults, setClientToolResults] = useState(/* @__PURE__ */ new Map());
848
+ const messagesRef = useRef(chatMessages);
849
+ messagesRef.current = chatMessages;
850
+ const initialMessagesRef = useRef(initialMessages);
851
+ initialMessagesRef.current = initialMessages;
852
+ const seededInitialMessagesKeyRef = useRef(null);
853
+ const markInitialMessagesSeeded = useCallback(() => {
854
+ seededInitialMessagesKeyRef.current = initialMessagesCacheKey;
855
+ }, [initialMessagesCacheKey]);
856
+ useEffect(() => {
857
+ if (!initialMessagesPromise) return;
858
+ if (seededInitialMessagesKeyRef.current === initialMessagesCacheKey) return;
859
+ markInitialMessagesSeeded();
860
+ setMessages((prevMessages) => prependMissingHydratedMessages(initialMessagesRef.current, prevMessages));
861
+ }, [
862
+ initialMessagesCacheKey,
863
+ initialMessagesPromise,
864
+ markInitialMessagesSeeded,
865
+ setMessages
866
+ ]);
867
+ const localResponseMessageIdsRef = useRef(/* @__PURE__ */ new Map());
868
+ const protectedStreamingAssistantRef = useRef(null);
869
+ const preserveProtectedStreamingAssistant = useCallback((messages) => {
870
+ const protection = protectedStreamingAssistantRef.current;
871
+ if (!protection) return [...messages];
872
+ const protectedIndex = messages.findIndex((message) => message.id === protection.assistantId);
873
+ if (protectedIndex >= 0 && messages.slice(protectedIndex + 1).some((message) => message.role === "assistant")) {
874
+ protectedStreamingAssistantRef.current = null;
875
+ return [...messages];
876
+ }
877
+ const protectedAssistant = messagesRef.current.find((message) => message.id === protection.assistantId) ?? messages.find((message) => message.id === protection.assistantId);
878
+ if (!protectedAssistant) return [...messages];
879
+ return [...messages.filter((message) => message.id !== protection.assistantId), protectedAssistant];
880
+ }, []);
881
+ const protectStreamingAssistantTail = useCallback(() => {
882
+ if (statusRef.current !== "streaming") return;
883
+ const assistantInfo = findLastAssistantMessage(messagesRef.current);
884
+ if (!assistantInfo) return;
885
+ if (protectedStreamingAssistantRef.current?.assistantId !== assistantInfo.message.id) protectedStreamingAssistantRef.current = {
886
+ assistantId: assistantInfo.message.id,
887
+ anchorMessageId: messagesRef.current[assistantInfo.index - 1]?.id ?? null
888
+ };
889
+ setMessages((prevMessages) => {
890
+ const protection = protectedStreamingAssistantRef.current;
891
+ if (!protection) return prevMessages;
892
+ return moveMessageToEnd(prevMessages, protection.assistantId);
893
+ });
894
+ }, [setMessages]);
895
+ const restoreProtectedStreamingAssistant = useCallback((assistantId) => {
896
+ const protection = protectedStreamingAssistantRef.current;
897
+ if (!protection || assistantId !== void 0 && protection.assistantId !== assistantId) return;
898
+ protectedStreamingAssistantRef.current = null;
899
+ setMessages((prevMessages) => {
900
+ const sourceIdx = prevMessages.findIndex((m) => m.id === protection.assistantId);
901
+ if (sourceIdx < 0) return prevMessages;
902
+ const result = [...prevMessages];
903
+ const [msg] = result.splice(sourceIdx, 1);
904
+ if (!msg) return prevMessages;
905
+ if (protection.anchorMessageId === null) result.unshift(msg);
906
+ else {
907
+ const anchorIdx = result.findIndex((m) => m.id === protection.anchorMessageId);
908
+ result.splice(anchorIdx >= 0 ? anchorIdx + 1 : sourceIdx, 0, msg);
909
+ }
910
+ return result;
911
+ });
912
+ }, [setMessages]);
913
+ const resetMatchingHydratedAssistantForReplay = useCallback((messageId) => {
914
+ setMessages((prevMessages) => {
915
+ const lastMessage = prevMessages[prevMessages.length - 1];
916
+ if (!lastMessage || lastMessage.role !== "assistant" || lastMessage.id !== messageId) return prevMessages;
917
+ replayHydratedAssistantMessageIdsRef.current.add(messageId);
918
+ const next = [...prevMessages];
919
+ next[next.length - 1] = {
920
+ ...lastMessage,
921
+ parts: []
922
+ };
923
+ return next;
924
+ });
925
+ }, [setMessages]);
926
+ const collapseHydratedReplayTextParts = useCallback((message) => {
927
+ const parts = message.parts;
928
+ const nextParts = parts.filter((part, index) => {
929
+ if (part.type !== "text" || !("text" in part) || !part.text) return true;
930
+ return !parts.some((candidate, candidateIndex) => {
931
+ if (candidateIndex <= index) return false;
932
+ if (candidate.type !== "text" || !("text" in candidate) || !candidate.text) return false;
933
+ return candidate.text.startsWith(part.text);
934
+ });
935
+ });
936
+ return nextParts.length === parts.length ? message : {
937
+ ...message,
938
+ parts: nextParts
939
+ };
940
+ }, []);
941
+ useEffect(() => {
942
+ if (replayHydratedAssistantMessageIdsRef.current.size === 0) return;
943
+ const idsToCollapse = new Set(chatMessages.filter((message) => replayHydratedAssistantMessageIdsRef.current.has(message.id) && message.role === "assistant" && collapseHydratedReplayTextParts(message) !== message).map((message) => message.id));
944
+ if (idsToCollapse.size === 0) return;
945
+ setMessages((prevMessages) => {
946
+ let changed = false;
947
+ const nextMessages = prevMessages.map((message) => {
948
+ if (!idsToCollapse.has(message.id)) return message;
949
+ const nextMessage = collapseHydratedReplayTextParts(message);
950
+ if (nextMessage !== message) changed = true;
951
+ return nextMessage;
952
+ });
953
+ return changed ? nextMessages : prevMessages;
954
+ });
955
+ }, [
956
+ chatMessages,
957
+ collapseHydratedReplayTextParts,
958
+ setMessages
959
+ ]);
960
+ const resetLocalChatState = useCallback(() => {
961
+ markInitialMessagesSeeded();
962
+ setMessages([]);
963
+ setClientToolResults(/* @__PURE__ */ new Map());
964
+ setPendingOnToolCallIds(/* @__PURE__ */ new Set());
965
+ resetToolContinuation();
966
+ processedToolCalls.current.clear();
967
+ localResponseMessageIdsRef.current.clear();
968
+ pendingReplayResumeRequestIdsRef.current.clear();
969
+ fallbackAckedResumeRequestIdsRef.current.clear();
970
+ replayHydratedAssistantMessageIdsRef.current.clear();
971
+ protectedStreamingAssistantRef.current = null;
972
+ }, [
973
+ markInitialMessagesSeeded,
974
+ setMessages,
975
+ resetToolContinuation
976
+ ]);
977
+ const sendMessageWithStreamingProtection = useCallback(async (message, options) => {
978
+ const request = sendMessage(message, options);
979
+ if (message !== void 0 && !(typeof message === "object" && message !== null && "messageId" in message && message.messageId != null)) protectStreamingAssistantTail();
980
+ return request;
981
+ }, [sendMessage, protectStreamingAssistantTail]);
982
+ const lastMessage = chatMessages[chatMessages.length - 1];
983
+ const pendingConfirmations = (() => {
984
+ if (!lastMessage || lastMessage.role !== "assistant") return {
985
+ messageId: void 0,
986
+ toolCallIds: /* @__PURE__ */ new Set()
987
+ };
988
+ const pendingIds = /* @__PURE__ */ new Set();
989
+ for (const part of lastMessage.parts ?? []) if (isToolUIPart(part) && part.state === "input-available" && toolsRequiringConfirmation.includes(getToolName(part))) pendingIds.add(part.toolCallId);
990
+ return {
991
+ messageId: lastMessage.id,
992
+ toolCallIds: pendingIds
993
+ };
994
+ })();
995
+ const pendingConfirmationsRef = useRef(pendingConfirmations);
996
+ pendingConfirmationsRef.current = pendingConfirmations;
997
+ const [pendingOnToolCallIds, setPendingOnToolCallIds] = useState(() => /* @__PURE__ */ new Set());
998
+ const finishOnToolCall = useCallback((toolCallId) => {
999
+ setPendingOnToolCallIds((prev) => {
1000
+ if (!prev.has(toolCallId)) return prev;
1001
+ const next = new Set(prev);
1002
+ next.delete(toolCallId);
1003
+ return next;
1004
+ });
1005
+ }, []);
1006
+ useEffect(() => {
1007
+ if (!experimental_automaticToolResolution) return;
1008
+ if (isResolvingToolsRef.current) return;
1009
+ const lastMsg = chatMessages[chatMessages.length - 1];
1010
+ if (!lastMsg || lastMsg.role !== "assistant") return;
1011
+ const toolCalls = lastMsg.parts.filter((part) => isToolUIPart(part) && part.state === "input-available" && !processedToolCalls.current.has(part.toolCallId));
1012
+ if (toolCalls.length > 0) {
1013
+ const currentTools = toolsRef.current;
1014
+ const toolCallsToResolve = toolCalls.filter((part) => isToolUIPart(part) && !toolsRequiringConfirmation.includes(getToolName(part)) && currentTools?.[getToolName(part)]?.execute);
1015
+ if (toolCallsToResolve.length > 0) {
1016
+ isResolvingToolsRef.current = true;
1017
+ (async () => {
1018
+ try {
1019
+ const toolResults = [];
1020
+ for (const part of toolCallsToResolve) if (isToolUIPart(part)) {
1021
+ let toolOutput = null;
1022
+ const toolName = getToolName(part);
1023
+ const tool = currentTools?.[toolName];
1024
+ if (tool?.execute && part.input !== void 0) try {
1025
+ toolOutput = await tool.execute(part.input);
1026
+ } catch (error) {
1027
+ toolOutput = `Error executing tool: ${error instanceof Error ? error.message : String(error)}`;
1028
+ }
1029
+ processedToolCalls.current.add(part.toolCallId);
1030
+ toolResults.push({
1031
+ toolCallId: part.toolCallId,
1032
+ toolName,
1033
+ output: toolOutput
1034
+ });
1035
+ }
1036
+ if (toolResults.length > 0) {
1037
+ const clientToolSchemas = extractClientToolSchemas(currentTools);
1038
+ for (const result of toolResults) agentRef.current.send(JSON.stringify({
1039
+ type: "cf_agent_tool_result",
1040
+ toolCallId: result.toolCallId,
1041
+ toolName: result.toolName,
1042
+ output: result.output,
1043
+ autoContinue: autoContinueAfterToolResult,
1044
+ clientTools: clientToolSchemas
1045
+ }));
1046
+ await Promise.all(toolResults.map((result) => addToolResult({
1047
+ tool: result.toolName,
1048
+ toolCallId: result.toolCallId,
1049
+ output: result.output
1050
+ })));
1051
+ setClientToolResults((prev) => {
1052
+ const newMap = new Map(prev);
1053
+ for (const result of toolResults) newMap.set(result.toolCallId, result.output);
1054
+ return newMap;
1055
+ });
1056
+ startToolContinuation();
1057
+ }
1058
+ } finally {
1059
+ isResolvingToolsRef.current = false;
1060
+ setToolResolutionTrigger((c) => c + 1);
1061
+ }
1062
+ })();
1063
+ }
1064
+ }
1065
+ }, [
1066
+ chatMessages,
1067
+ experimental_automaticToolResolution,
1068
+ addToolResult,
1069
+ toolsRequiringConfirmation,
1070
+ autoContinueAfterToolResult,
1071
+ startToolContinuation,
1072
+ toolResolutionTrigger
1073
+ ]);
1074
+ const sendToolOutputToServer = useCallback((toolCallId, toolName, output, state, errorText) => {
1075
+ const shouldAutoContinue = state === "output-error" ? false : autoContinueAfterToolResult;
1076
+ agentRef.current.send(JSON.stringify({
1077
+ type: "cf_agent_tool_result",
1078
+ toolCallId,
1079
+ toolName,
1080
+ output,
1081
+ ...state ? { state } : {},
1082
+ ...errorText !== void 0 ? { errorText } : {},
1083
+ autoContinue: shouldAutoContinue,
1084
+ clientTools: toolsRef.current ? extractClientToolSchemas(toolsRef.current) : void 0
1085
+ }));
1086
+ if (state !== "output-error") setClientToolResults((prev) => new Map(prev).set(toolCallId, output));
1087
+ if (shouldAutoContinue) startToolContinuation();
1088
+ }, [autoContinueAfterToolResult, startToolContinuation]);
1089
+ const sendToolApprovalToServer = useCallback((toolCallId, approved) => {
1090
+ agentRef.current.send(JSON.stringify({
1091
+ type: "cf_agent_tool_approval",
1092
+ toolCallId,
1093
+ approved,
1094
+ autoContinue: autoContinueAfterToolResult
1095
+ }));
1096
+ if (autoContinueAfterToolResult) startToolContinuation();
1097
+ }, [autoContinueAfterToolResult, startToolContinuation]);
1098
+ useEffect(() => {
1099
+ const currentOnToolCall = onToolCallRef.current;
1100
+ if (!currentOnToolCall) return;
1101
+ const lastMsg = chatMessages[chatMessages.length - 1];
1102
+ if (!lastMsg || lastMsg.role !== "assistant") return;
1103
+ const pendingToolCalls = lastMsg.parts.filter((part) => isToolUIPart(part) && part.state === "input-available" && !processedToolCalls.current.has(part.toolCallId));
1104
+ for (const part of pendingToolCalls) if (isToolUIPart(part)) {
1105
+ const toolCallId = part.toolCallId;
1106
+ const toolName = getToolName(part);
1107
+ processedToolCalls.current.add(toolCallId);
1108
+ setPendingOnToolCallIds((prev) => {
1109
+ if (prev.has(toolCallId)) return prev;
1110
+ const next = new Set(prev);
1111
+ next.add(toolCallId);
1112
+ return next;
1113
+ });
1114
+ const addToolOutput = (opts) => {
1115
+ sendToolOutputToServer(opts.toolCallId, toolName, opts.output, opts.state, opts.errorText);
1116
+ addToolResult({
1117
+ tool: toolName,
1118
+ toolCallId: opts.toolCallId,
1119
+ output: opts.state === "output-error" ? opts.errorText ?? "Tool execution denied by user" : opts.output
1120
+ });
1121
+ };
1122
+ let result;
1123
+ try {
1124
+ result = currentOnToolCall({
1125
+ toolCall: {
1126
+ toolCallId,
1127
+ toolName,
1128
+ input: part.input
1129
+ },
1130
+ addToolOutput
1131
+ });
1132
+ } catch (error) {
1133
+ finishOnToolCall(toolCallId);
1134
+ throw error;
1135
+ }
1136
+ Promise.resolve(result).finally(() => {
1137
+ finishOnToolCall(toolCallId);
1138
+ });
1139
+ }
1140
+ }, [
1141
+ chatMessages,
1142
+ sendToolOutputToServer,
1143
+ addToolResult,
1144
+ finishOnToolCall
1145
+ ]);
1146
+ const streamStateRef = useRef({ status: "idle" });
1147
+ const [isServerStreaming, setIsServerStreaming] = useState(false);
1148
+ const [isRecovering, setIsRecovering] = useState(false);
1149
+ useEffect(() => {
1150
+ const localResponseIds = localResponseMessageIdsRef.current;
1151
+ /**
1152
+ * Unified message handler that parses JSON once and dispatches based on type.
1153
+ * Avoids duplicate parsing overhead from separate listeners.
1154
+ */
1155
+ function onAgentMessage(event) {
1156
+ if (typeof event.data !== "string") return;
1157
+ let data;
1158
+ try {
1159
+ data = JSON.parse(event.data);
1160
+ } catch (_error) {
1161
+ return;
1162
+ }
1163
+ switch (data.type) {
1164
+ case "cf_agent_chat_clear":
1165
+ streamStateRef.current = transition(streamStateRef.current, { type: "clear" }).state;
1166
+ setIsServerStreaming(false);
1167
+ setIsRecovering(false);
1168
+ resetLocalChatState();
1169
+ break;
1170
+ case "cf_agent_chat_recovering":
1171
+ setIsRecovering(Boolean(data.recovering));
1172
+ break;
1173
+ case "cf_agent_chat_messages": {
1174
+ let next = preserveProtectedStreamingAssistant(data.messages);
1175
+ const observed = streamStateRef.current;
1176
+ if (observed.status === "observing" && observed.accumulator.parts.length > 0) {
1177
+ const snapshotIdx = next.findIndex((m) => m.id === observed.accumulator.messageId);
1178
+ const snapshotParts = snapshotIdx >= 0 ? next[snapshotIdx].parts.length : 0;
1179
+ if (observed.accumulator.parts.length >= snapshotParts) next = observed.accumulator.mergeInto(next);
1180
+ }
1181
+ setMessages(next);
1182
+ break;
1183
+ }
1184
+ case "cf_agent_message_updated":
1185
+ setMessages((prevMessages) => {
1186
+ const updatedMessage = data.message;
1187
+ let idx = prevMessages.findIndex((m) => m.id === updatedMessage.id);
1188
+ if (idx < 0) {
1189
+ const updatedToolCallIds = new Set(updatedMessage.parts.filter((p) => "toolCallId" in p && p.toolCallId).map((p) => p.toolCallId));
1190
+ if (updatedToolCallIds.size > 0) idx = prevMessages.findIndex((m) => m.parts.some((p) => "toolCallId" in p && updatedToolCallIds.has(p.toolCallId)));
1191
+ }
1192
+ if (idx >= 0) {
1193
+ const updated = [...prevMessages];
1194
+ updated[idx] = {
1195
+ ...updatedMessage,
1196
+ id: prevMessages[idx].id
1197
+ };
1198
+ return updated;
1199
+ }
1200
+ return prevMessages;
1201
+ });
1202
+ break;
1203
+ case "cf_agent_stream_resume_none":
1204
+ customTransport.handleStreamResumeNone();
1205
+ break;
1206
+ case "cf_agent_stream_pending":
1207
+ customTransport.handleStreamPending();
1208
+ break;
1209
+ case "cf_agent_stream_resuming": {
1210
+ const isEarlyToolContinuation = resumingToolContinuationRef.current && !customTransport.isAwaitingResume();
1211
+ if (!resume && !customTransport.isAwaitingResume()) {
1212
+ if (!isEarlyToolContinuation) return;
1213
+ }
1214
+ if (!resumingToolContinuationRef.current) pendingReplayResumeRequestIdsRef.current.add(data.id);
1215
+ if (customTransport.handleStreamResuming(data)) return;
1216
+ if (localRequestIdsRef.current.has(data.id)) return;
1217
+ if (fallbackAckedResumeRequestIdsRef.current.has(data.id)) return;
1218
+ if (isEarlyToolContinuation) {
1219
+ pendingToolContinuationRef.current = false;
1220
+ observedToolContinuationRequestIdRef.current = data.id;
1221
+ if (continuationLaunchTimerRef.current) {
1222
+ clearTimeout(continuationLaunchTimerRef.current);
1223
+ continuationLaunchTimerRef.current = null;
1224
+ }
1225
+ }
1226
+ streamStateRef.current = transition(streamStateRef.current, {
1227
+ type: "resume-fallback",
1228
+ streamId: data.id,
1229
+ messageId: nanoid()
1230
+ }).state;
1231
+ customTransport.observeServerTurn(data.id);
1232
+ setIsServerStreaming(true);
1233
+ setIsRecovering(false);
1234
+ fallbackAckedResumeRequestIdsRef.current.add(data.id);
1235
+ agentRef.current.send(JSON.stringify({
1236
+ type: "cf_agent_stream_resume_ack",
1237
+ id: data.id
1238
+ }));
1239
+ break;
1240
+ }
1241
+ case "cf_agent_use_chat_response": {
1242
+ if (localRequestIdsRef.current.has(data.id)) {
1243
+ if (data.body?.trim()) try {
1244
+ const chunkData = JSON.parse(data.body);
1245
+ if (chunkData.type === "start" && typeof chunkData.messageId === "string") {
1246
+ localResponseIds.set(data.id, chunkData.messageId);
1247
+ if (!data.continuation) {
1248
+ if (protectedStreamingAssistantRef.current?.assistantId !== chunkData.messageId) {
1249
+ const msgs = messagesRef.current;
1250
+ const idx = msgs.findIndex((m) => m.id === chunkData.messageId);
1251
+ const anchorMessageId = idx >= 0 ? msgs[idx - 1]?.id ?? null : msgs[msgs.length - 1]?.id ?? null;
1252
+ protectedStreamingAssistantRef.current = {
1253
+ assistantId: chunkData.messageId,
1254
+ anchorMessageId
1255
+ };
1256
+ }
1257
+ }
1258
+ if (data.replay && !data.continuation && !resumingToolContinuationRef.current && observedToolContinuationRequestIdRef.current !== data.id) {
1259
+ pendingReplayResumeRequestIdsRef.current.delete(data.id);
1260
+ resetMatchingHydratedAssistantForReplay(chunkData.messageId);
1261
+ }
1262
+ }
1263
+ } catch {}
1264
+ if (data.done || data.replayComplete) pendingReplayResumeRequestIdsRef.current.delete(data.id);
1265
+ if (data.done) {
1266
+ if (streamStateRef.current.status === "observing" && streamStateRef.current.streamId === data.id) {
1267
+ streamStateRef.current = { status: "idle" };
1268
+ setIsServerStreaming(false);
1269
+ }
1270
+ customTransport.handleServerTurnCompleted(data.id);
1271
+ restoreProtectedStreamingAssistant(localResponseIds.get(data.id));
1272
+ localResponseIds.delete(data.id);
1273
+ localRequestIdsRef.current.delete(data.id);
1274
+ fallbackAckedResumeRequestIdsRef.current.delete(data.id);
1275
+ }
1276
+ return;
1277
+ }
1278
+ let chunkData;
1279
+ if (data.replay && streamStateRef.current.status !== "observing" && !pendingReplayResumeRequestIdsRef.current.has(data.id)) return;
1280
+ if (data.body?.trim()) try {
1281
+ chunkData = JSON.parse(data.body);
1282
+ if (data.replay && !data.continuation && !resumingToolContinuationRef.current && observedToolContinuationRequestIdRef.current !== data.id && typeof chunkData.messageId === "string" && chunkData.type === "start") {
1283
+ pendingReplayResumeRequestIdsRef.current.delete(data.id);
1284
+ resetMatchingHydratedAssistantForReplay(chunkData.messageId);
1285
+ }
1286
+ if (typeof chunkData.type === "string" && chunkData.type.startsWith("data-") && onDataRef.current) onDataRef.current(chunkData);
1287
+ } catch (parseError) {
1288
+ console.warn("[useAgentChat] Failed to parse stream chunk:", parseError instanceof Error ? parseError.message : parseError, "body:", data.body?.slice(0, 100));
1289
+ }
1290
+ if (data.done || data.replayComplete) pendingReplayResumeRequestIdsRef.current.delete(data.id);
1291
+ if (data.done) {
1292
+ customTransport.handleServerTurnCompleted(data.id);
1293
+ fallbackAckedResumeRequestIdsRef.current.delete(data.id);
1294
+ setIsRecovering(false);
1295
+ }
1296
+ const completedObservedToolContinuation = data.done && observedToolContinuationRequestIdRef.current === data.id;
1297
+ const result = transition(streamStateRef.current, {
1298
+ type: "response",
1299
+ streamId: data.id,
1300
+ messageId: nanoid(),
1301
+ chunkData,
1302
+ done: data.done,
1303
+ error: data.error,
1304
+ replay: data.replay,
1305
+ replayComplete: data.replayComplete,
1306
+ continuation: data.continuation,
1307
+ currentMessages: data.continuation ? messagesRef.current : void 0
1308
+ });
1309
+ streamStateRef.current = result.state;
1310
+ if (result.messagesUpdate) setMessages(result.messagesUpdate);
1311
+ setIsServerStreaming(result.isStreaming);
1312
+ if (completedObservedToolContinuation) resetToolContinuation();
1313
+ break;
1314
+ }
1315
+ }
1316
+ }
1317
+ const fallbackAckedResumeRequestIds = fallbackAckedResumeRequestIdsRef.current;
1318
+ function onAgentClose() {
1319
+ fallbackAckedResumeRequestIds.clear();
1320
+ }
1321
+ function onAgentOpen() {
1322
+ if (!hasConnectedOnceRef.current) {
1323
+ hasConnectedOnceRef.current = true;
1324
+ return;
1325
+ }
1326
+ if (!resume || statusRef.current !== "ready" || resumingToolContinuationRef.current || customTransport.isAwaitingResume()) return;
1327
+ Promise.resolve(resumeStreamRef.current?.()).catch(() => {});
1328
+ }
1329
+ agent.addEventListener("message", onAgentMessage);
1330
+ agent.addEventListener("close", onAgentClose);
1331
+ agent.addEventListener("open", onAgentOpen);
1332
+ return () => {
1333
+ agent.removeEventListener("message", onAgentMessage);
1334
+ agent.removeEventListener("close", onAgentClose);
1335
+ agent.removeEventListener("open", onAgentOpen);
1336
+ fallbackAckedResumeRequestIds.clear();
1337
+ streamStateRef.current = { status: "idle" };
1338
+ setIsServerStreaming(false);
1339
+ setIsRecovering(false);
1340
+ protectedStreamingAssistantRef.current = null;
1341
+ localResponseIds.clear();
1342
+ };
1343
+ }, [
1344
+ agent,
1345
+ setMessages,
1346
+ resume,
1347
+ customTransport,
1348
+ preserveProtectedStreamingAssistant,
1349
+ resetToolContinuation,
1350
+ resetMatchingHydratedAssistantForReplay,
1351
+ restoreProtectedStreamingAssistant,
1352
+ resetLocalChatState
1353
+ ]);
1354
+ const addToolResultAndSendMessage = async (args) => {
1355
+ const { toolCallId } = args;
1356
+ const toolName = "tool" in args ? args.tool : "";
1357
+ const output = "output" in args ? args.output : void 0;
1358
+ agentRef.current.send(JSON.stringify({
1359
+ type: "cf_agent_tool_result",
1360
+ toolCallId,
1361
+ toolName,
1362
+ output,
1363
+ autoContinue: autoContinueAfterToolResult,
1364
+ clientTools: toolsRef.current ? extractClientToolSchemas(toolsRef.current) : void 0
1365
+ }));
1366
+ setClientToolResults((prev) => new Map(prev).set(toolCallId, output));
1367
+ addToolResult(args);
1368
+ if (autoContinueAfterToolResult) startToolContinuation();
1369
+ if (!autoContinueAfterToolResult) {
1370
+ if (!autoSendAfterAllConfirmationsResolved) {
1371
+ sendMessage();
1372
+ return;
1373
+ }
1374
+ const pending = pendingConfirmationsRef.current?.toolCallIds;
1375
+ if (!pending) {
1376
+ sendMessage();
1377
+ return;
1378
+ }
1379
+ const wasLast = pending.size === 1 && pending.has(toolCallId);
1380
+ if (pending.has(toolCallId)) pending.delete(toolCallId);
1381
+ if (wasLast || pending.size === 0) sendMessage();
1382
+ }
1383
+ };
1384
+ const addToolApprovalResponseAndNotifyServer = (args) => {
1385
+ const { id: approvalId, approved } = args;
1386
+ let toolCallId;
1387
+ for (const msg of messagesRef.current) {
1388
+ for (const part of msg.parts) if ("toolCallId" in part && "approval" in part && part.approval?.id === approvalId) {
1389
+ toolCallId = part.toolCallId;
1390
+ break;
1391
+ }
1392
+ if (toolCallId) break;
1393
+ }
1394
+ if (toolCallId) sendToolApprovalToServer(toolCallId, approved);
1395
+ else console.warn(`[useAgentChat] addToolApprovalResponse: Could not find toolCallId for approval ID "${approvalId}". Server will not be notified, which may cause duplicate messages.`);
1396
+ addToolApprovalResponse(args);
1397
+ };
1398
+ const messagesWithToolResults = useMemo(() => {
1399
+ if (clientToolResults.size === 0) return chatMessages;
1400
+ return chatMessages.map((msg) => ({
1401
+ ...msg,
1402
+ parts: msg.parts.map((p) => {
1403
+ if (!("toolCallId" in p) || !("state" in p) || p.state !== "input-available" || !clientToolResults.has(p.toolCallId)) return p;
1404
+ return {
1405
+ ...p,
1406
+ state: "output-available",
1407
+ output: clientToolResults.get(p.toolCallId)
1408
+ };
1409
+ })
1410
+ }));
1411
+ }, [chatMessages, clientToolResults]);
1412
+ useEffect(() => {
1413
+ const currentToolCallIds = /* @__PURE__ */ new Set();
1414
+ for (const msg of chatMessages) for (const part of msg.parts) if ("toolCallId" in part && part.toolCallId) currentToolCallIds.add(part.toolCallId);
1415
+ setClientToolResults((prev) => {
1416
+ if (prev.size === 0) return prev;
1417
+ let hasStaleEntries = false;
1418
+ for (const toolCallId of prev.keys()) if (!currentToolCallIds.has(toolCallId)) {
1419
+ hasStaleEntries = true;
1420
+ break;
1421
+ }
1422
+ if (!hasStaleEntries) return prev;
1423
+ const newMap = /* @__PURE__ */ new Map();
1424
+ for (const [id, output] of prev) if (currentToolCallIds.has(id)) newMap.set(id, output);
1425
+ return newMap;
1426
+ });
1427
+ for (const toolCallId of processedToolCalls.current) if (!currentToolCallIds.has(toolCallId)) processedToolCalls.current.delete(toolCallId);
1428
+ }, [chatMessages]);
1429
+ const addToolOutput = useCallback((opts) => {
1430
+ const toolName = opts.toolName ?? "";
1431
+ sendToolOutputToServer(opts.toolCallId, toolName, opts.output, opts.state, opts.errorText);
1432
+ addToolResult({
1433
+ tool: toolName,
1434
+ toolCallId: opts.toolCallId,
1435
+ output: opts.state === "output-error" ? opts.errorText ?? "Tool execution denied by user" : opts.output
1436
+ });
1437
+ }, [sendToolOutputToServer, addToolResult]);
1438
+ const lastAssistantMessage = messagesWithToolResults[messagesWithToolResults.length - 1];
1439
+ const hasPendingClientToolCalls = (() => {
1440
+ if (pendingOnToolCallIds.size === 0 && !tools) return false;
1441
+ if (!lastAssistantMessage || lastAssistantMessage.role !== "assistant") return false;
1442
+ for (const part of lastAssistantMessage.parts) {
1443
+ if (!isToolUIPart(part)) continue;
1444
+ if (part.state !== "input-available") continue;
1445
+ const toolName = getToolName(part);
1446
+ if (toolsRequiringConfirmation.includes(toolName)) continue;
1447
+ if (pendingOnToolCallIds.has(part.toolCallId)) return true;
1448
+ if (tools?.[toolName]?.execute) return true;
1449
+ }
1450
+ return false;
1451
+ })();
1452
+ const effectiveIsServerStreaming = isServerStreaming || hasPendingClientToolCalls;
1453
+ const isStreaming = status === "streaming" || effectiveIsServerStreaming;
1454
+ return {
1455
+ ...useChatHelpers,
1456
+ messages: messagesWithToolResults,
1457
+ isServerStreaming: effectiveIsServerStreaming,
1458
+ isStreaming,
1459
+ /**
1460
+ * True while a durable chat turn is being recovered (interrupted by a
1461
+ * deploy/eviction or a stream-stall watchdog abort and now resuming, #1620).
1462
+ * Distinct from `isStreaming` — a recovering turn isn't producing tokens
1463
+ * yet. Render a "recovering…" hint; most UIs treat `isStreaming ||
1464
+ * isRecovering` as "busy". Cleared automatically on the next stream/terminal.
1465
+ */
1466
+ isRecovering,
1467
+ isToolContinuation,
1468
+ connectionError: agent.connectionError ?? null,
1469
+ sendMessage: sendMessageWithStreamingProtection,
1470
+ stop: stopWithToolContinuationAbort,
1471
+ /**
1472
+ * Provide output for a tool call. Use this for tools that require user interaction
1473
+ * or client-side execution.
1474
+ */
1475
+ addToolOutput,
1476
+ /**
1477
+ * @deprecated Use `addToolOutput` instead.
1478
+ */
1479
+ addToolResult: addToolResultAndSendMessage,
1480
+ /**
1481
+ * Respond to a tool approval request. Use this for tools with `needsApproval`.
1482
+ * This wrapper notifies the server before updating local state, preventing
1483
+ * duplicate messages when sendMessage() is called afterward.
1484
+ */
1485
+ addToolApprovalResponse: addToolApprovalResponseAndNotifyServer,
1486
+ clearHistory: () => {
1487
+ resetLocalChatState();
1488
+ agent.send(JSON.stringify({ type: "cf_agent_chat_clear" }));
1489
+ },
1490
+ setMessages: (messagesOrUpdater) => {
1491
+ let resolvedMessages;
1492
+ if (typeof messagesOrUpdater === "function") resolvedMessages = messagesOrUpdater(messagesRef.current);
1493
+ else resolvedMessages = messagesOrUpdater;
1494
+ if (resolvedMessages.length === 0) markInitialMessagesSeeded();
1495
+ setMessages(resolvedMessages);
1496
+ if (syncMessagesToServer) agent.send(JSON.stringify({
1497
+ messages: resolvedMessages,
1498
+ type: "cf_agent_chat_messages"
1499
+ }));
1500
+ }
1501
+ };
1502
+ }
1503
+ //#endregion
1504
+ export { WebSocketChatTransport, detectToolsRequiringConfirmation, extractClientToolSchemas, getAgentMessages, getToolApproval, getToolCallId, getToolInput, getToolOutput, getToolPartState, useAgentChat };
1505
+
1506
+ //# sourceMappingURL=react.js.map