agents 0.24.0 → 0.26.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 (170) hide show
  1. package/dist/{agent-routing-B2XLNMxq.d.ts → agent-routing-0DR0Tk2v.d.ts} +205 -132
  2. package/dist/agent-routing.d.ts +1 -1
  3. package/dist/agent-routing.js.map +1 -1
  4. package/dist/agent-tool-types.d.ts +28 -22
  5. package/dist/agent-tool-types.js +23 -1
  6. package/dist/agent-tool-types.js.map +1 -1
  7. package/dist/{agent-tools-y7zLfw4Q.js → agent-tools-CdJC-8Dc.js} +72 -9
  8. package/dist/agent-tools-CdJC-8Dc.js.map +1 -0
  9. package/dist/{agent-tools-Dj5Mzlw7.d.ts → agent-tools-YrATZJu2.d.ts} +34 -20
  10. package/dist/agent-tools.d.ts +20 -18
  11. package/dist/agent-tools.js.map +1 -1
  12. package/dist/ai-fm7xx1BK.js +312 -0
  13. package/dist/ai-fm7xx1BK.js.map +1 -0
  14. package/dist/base64-redaction-C1Uu29t5.js +59 -0
  15. package/dist/base64-redaction-C1Uu29t5.js.map +1 -0
  16. package/dist/browser/ai-sdk.d.ts +90 -0
  17. package/dist/browser/ai-sdk.js +51 -0
  18. package/dist/browser/ai-sdk.js.map +1 -0
  19. package/dist/browser/ai.d.ts +4 -4
  20. package/dist/browser/ai.js +1 -347
  21. package/dist/browser/index.d.ts +81 -79
  22. package/dist/browser/index.js +361 -2
  23. package/dist/browser/index.js.map +1 -0
  24. package/dist/browser/tanstack-ai.d.ts +96 -1
  25. package/dist/browser/tanstack-ai.js +62 -2
  26. package/dist/browser/tanstack-ai.js.map +1 -1
  27. package/dist/browser-DE3d-RLk.d.ts +275 -0
  28. package/dist/browser-tool-CQ10e6Kj.js +480 -0
  29. package/dist/browser-tool-CQ10e6Kj.js.map +1 -0
  30. package/dist/browser-tool-DuNdlwZ9.d.ts +73 -0
  31. package/dist/capability-B4WbF81e.js.map +1 -1
  32. package/dist/{capability-runner-BUBa6Ake.d.ts → capability-runner-DBAnGr6L.d.ts} +7 -2
  33. package/dist/chat/index.d.ts +340 -234
  34. package/dist/chat/index.js +592 -66
  35. package/dist/chat/index.js.map +1 -1
  36. package/dist/chat/react.d.ts +42 -2
  37. package/dist/chat/react.js +191 -35
  38. package/dist/chat/react.js.map +1 -1
  39. package/dist/chat/transport.d.ts +1 -1
  40. package/dist/chat/transport.js +1 -1
  41. package/dist/chat-sdk/index.d.ts +17 -3
  42. package/dist/chat-sdk/index.js +28 -2
  43. package/dist/chat-sdk/index.js.map +1 -1
  44. package/dist/{client-C9DAjZIY.js → client-4OlMIJx-.js} +102 -29
  45. package/dist/client-4OlMIJx-.js.map +1 -0
  46. package/dist/{client-BxdkCuVy.d.ts → client-je3koD9Z.d.ts} +45 -10
  47. package/dist/client.d.ts +7 -5
  48. package/dist/client.js +2 -2
  49. package/dist/{connector-CptFKzRh.js → connector-58XDGvc3.js} +251 -120
  50. package/dist/connector-58XDGvc3.js.map +1 -0
  51. package/dist/{connector-CkQD4MK3.d.ts → connector-Cl-VqPmA.d.ts} +244 -146
  52. package/dist/context/index.d.ts +29 -0
  53. package/dist/context/index.js +64 -17
  54. package/dist/context/index.js.map +1 -1
  55. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  56. package/dist/{current-agent-C4w86kcN.d.ts → current-agent-vAk_5JMt.d.ts} +4 -4
  57. package/dist/{durable-object-lifecycle-ei7M7lqB.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +15 -7
  58. package/dist/harness/pi/index.d.ts +344 -0
  59. package/dist/harness/pi/index.js +873 -0
  60. package/dist/harness/pi/index.js.map +1 -0
  61. package/dist/images-iiTDSyRu.js +1631 -0
  62. package/dist/images-iiTDSyRu.js.map +1 -0
  63. package/dist/{index-Dlgtd3Mj.d.ts → index-BreL0YrQ.d.ts} +2 -2
  64. package/dist/index-kbSUIer3.d.ts +343 -0
  65. package/dist/index.d.ts +46 -44
  66. package/dist/index.js +1 -1
  67. package/dist/internal_context.d.ts +1 -1
  68. package/dist/lifecycle/index.d.ts +3 -3
  69. package/dist/lifecycle/index.js +1 -1
  70. package/dist/{lifecycle-Mm_jQh7r.js → lifecycle-CFu6OV03.js} +39 -10
  71. package/dist/{lifecycle-Mm_jQh7r.js.map → lifecycle-CFu6OV03.js.map} +1 -1
  72. package/dist/mcp/client/index.d.ts +20 -20
  73. package/dist/mcp/client/x402.js +21 -19
  74. package/dist/mcp/client/x402.js.map +1 -1
  75. package/dist/mcp/index.d.ts +35 -35
  76. package/dist/mcp/index.js +1 -1
  77. package/dist/{message-builder-BymO4N_D.js → message-builder-DAl9AhHH.js} +89 -4
  78. package/dist/message-builder-DAl9AhHH.js.map +1 -0
  79. package/dist/models/ai-sdk/index.d.ts +353 -0
  80. package/dist/models/ai-sdk/index.js +2443 -0
  81. package/dist/models/ai-sdk/index.js.map +1 -0
  82. package/dist/models/pi-ai/index.d.ts +313 -0
  83. package/dist/models/pi-ai/index.js +1795 -0
  84. package/dist/models/pi-ai/index.js.map +1 -0
  85. package/dist/observability/ai/index.js +20 -4
  86. package/dist/observability/ai/index.js.map +1 -1
  87. package/dist/{protocol-B0nh6KNf.js → protocol-kHsr-f0X.js} +26 -12
  88. package/dist/{protocol-B0nh6KNf.js.map → protocol-kHsr-f0X.js.map} +1 -1
  89. package/dist/queue/index.d.ts +3 -3
  90. package/dist/queue/index.js +1 -1
  91. package/dist/{queue-BC9F5cUo.js → queue-BB-zfIYF.js} +2 -2
  92. package/dist/{queue-BC9F5cUo.js.map → queue-BB-zfIYF.js.map} +1 -1
  93. package/dist/react.d.ts +6 -6
  94. package/dist/react.js +160 -64
  95. package/dist/react.js.map +1 -1
  96. package/dist/{retries-D9B2UCq3.d.ts → retries-CV6AkXmQ.d.ts} +18 -9
  97. package/dist/retries.d.ts +11 -9
  98. package/dist/retries.js +11 -1
  99. package/dist/retries.js.map +1 -1
  100. package/dist/routing/index.d.ts +2 -2
  101. package/dist/routing/index.js +1 -1
  102. package/dist/{scheduler-D_KHqxBa.js → scheduler-DZ3xQfeK.js} +2 -2
  103. package/dist/{scheduler-D_KHqxBa.js.map → scheduler-DZ3xQfeK.js.map} +1 -1
  104. package/dist/{scheduler-BGq6M5Kd.d.ts → scheduler-RdACNXDP.d.ts} +3 -3
  105. package/dist/schedules/index.d.ts +1 -1
  106. package/dist/schedules/index.js +1 -1
  107. package/dist/serializable.d.ts +1 -1
  108. package/dist/sessions/index.d.ts +70 -3
  109. package/dist/sessions/index.js +276 -67
  110. package/dist/sessions/index.js.map +1 -1
  111. package/dist/skills/index.d.ts +29 -314
  112. package/dist/socket-address.d.ts +16 -0
  113. package/dist/socket-address.js +19 -0
  114. package/dist/socket-address.js.map +1 -0
  115. package/dist/{src-BNU3ZiJM.js → src-BZeu-SCD.js} +423 -115
  116. package/dist/src-BZeu-SCD.js.map +1 -0
  117. package/dist/state/index.d.ts +1 -1
  118. package/dist/state/index.js +1 -1
  119. package/dist/streams/index.d.ts +1 -1
  120. package/dist/streams/index.js +1 -1
  121. package/dist/{streams-BcE8JP2X.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
  122. package/dist/{streams-CCPRV6dt.js → streams-DW-UCOUc.js} +9 -2
  123. package/dist/streams-DW-UCOUc.js.map +1 -0
  124. package/dist/sub-routing.d.ts +12 -12
  125. package/dist/tasks/index.d.ts +1 -1
  126. package/dist/tasks/index.js +1 -1
  127. package/dist/{tasks-D4nLqVSI.js → tasks-68DESxf0.js} +2 -2
  128. package/dist/{tasks-D4nLqVSI.js.map → tasks-68DESxf0.js.map} +1 -1
  129. package/dist/{tasks-V2lCT8ZV.d.ts → tasks-9YxYtByh.d.ts} +2 -2
  130. package/dist/transport-PfFnt-Ld.d.ts +525 -0
  131. package/dist/{types-DCD6BNZH.d.ts → types-BA4anrjI.d.ts} +2 -2
  132. package/dist/voice/index.d.ts +2 -2
  133. package/dist/websockets/index.d.ts +23 -6
  134. package/dist/websockets/index.js +1 -1
  135. package/dist/{websockets-D7IzWbZb.js → websockets-Ci-LZFHH.js} +45 -20
  136. package/dist/{websockets-D7IzWbZb.js.map → websockets-Ci-LZFHH.js.map} +1 -1
  137. package/dist/wire-types-CnMt6_HR.js.map +1 -1
  138. package/dist/wire-types-CrjKvDyi.d.ts +300 -0
  139. package/dist/workflow-types.d.ts +25 -25
  140. package/dist/workflows.d.ts +21 -21
  141. package/dist/workflows.js +1 -1
  142. package/dist/{ws-chat-transport-UNRIS2xl.d.ts → ws-chat-transport-BbT1A5Lc.d.ts} +51 -2
  143. package/dist/{ws-chat-transport-rWwta645.js → ws-chat-transport-cqMKq3IY.js} +143 -16
  144. package/dist/ws-chat-transport-cqMKq3IY.js.map +1 -0
  145. package/docs/agent-tools.md +29 -4
  146. package/docs/browse-the-web.md +87 -5
  147. package/docs/chat-agents.md +124 -16
  148. package/docs/context.md +27 -0
  149. package/docs/durable-execution.md +2 -0
  150. package/docs/harnesses/pi.md +173 -0
  151. package/docs/human-in-the-loop.md +4 -0
  152. package/docs/index.md +3 -0
  153. package/docs/lifecycle.md +20 -4
  154. package/docs/models-pi-ai.md +325 -0
  155. package/docs/models.md +504 -0
  156. package/docs/resumable-streaming.md +2 -0
  157. package/docs/routing.md +13 -13
  158. package/docs/sessions.md +36 -1
  159. package/docs/state.md +1 -0
  160. package/docs/sub-agents.md +9 -0
  161. package/package.json +40 -2
  162. package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
  163. package/dist/browser/ai.js.map +0 -1
  164. package/dist/client-C9DAjZIY.js.map +0 -1
  165. package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
  166. package/dist/connector-CptFKzRh.js.map +0 -1
  167. package/dist/message-builder-BymO4N_D.js.map +0 -1
  168. package/dist/src-BNU3ZiJM.js.map +0 -1
  169. package/dist/streams-CCPRV6dt.js.map +0 -1
  170. package/dist/ws-chat-transport-rWwta645.js.map +0 -1
@@ -125,6 +125,100 @@ function appendDeltaText(target, source) {
125
125
  if ((target.type === "text-delta" || target.type === "reasoning-delta") && (source.type === "text-delta" || source.type === "reasoning-delta")) target.delta += source.delta;
126
126
  }
127
127
  //#endregion
128
+ //#region src/chat/replay-dedupe.ts
129
+ /**
130
+ * Requests the ledger remembers. A request that finishes while the client is
131
+ * disconnected never delivers the terminal frame that would forget it.
132
+ */
133
+ const MAX_TRACKED_REQUESTS = 32;
134
+ /** Highest chunk `seq` this client applied, per request id. */
135
+ var AppliedChunkLedger = class {
136
+ constructor() {
137
+ this.applied = /* @__PURE__ */ new Map();
138
+ }
139
+ record(requestId, seq) {
140
+ if (seq === void 0) return;
141
+ const applied = Math.max(seq, this.applied.get(requestId) ?? -1);
142
+ this.applied.delete(requestId);
143
+ this.applied.set(requestId, applied);
144
+ if (this.applied.size > MAX_TRACKED_REQUESTS) {
145
+ const oldest = this.applied.keys().next().value;
146
+ if (oldest !== void 0) this.applied.delete(oldest);
147
+ }
148
+ }
149
+ /** Whether `frame` replays a continuation chunk this client already applied. */
150
+ isAppliedReplay(requestId, frame) {
151
+ return frame.replay === true && frame.continuation === true && frame.seq !== void 0 && frame.seq <= (this.applied.get(requestId) ?? -1);
152
+ }
153
+ forget(requestId) {
154
+ this.applied.delete(requestId);
155
+ }
156
+ clear() {
157
+ this.applied.clear();
158
+ }
159
+ };
160
+ /**
161
+ * Filters one resumed stream's frames for a consumer that needs every part
162
+ * opened before its deltas arrive (the AI SDK UI message stream). Applied
163
+ * replay chunks are dropped; a part that was still open when they end is
164
+ * re-opened with its original start chunk, so the new deltas have a part to
165
+ * land in. The re-opened part renders as a second, adjacent part until the
166
+ * server's final message replaces it.
167
+ */
168
+ var ContinuationReplayFilter = class {
169
+ constructor(ledger, requestId) {
170
+ this.ledger = ledger;
171
+ this.requestId = requestId;
172
+ this.openParts = /* @__PURE__ */ new Map();
173
+ }
174
+ /** The frames to apply in place of `frame`. */
175
+ frames(frame) {
176
+ if (this.ledger.isAppliedReplay(this.requestId, frame)) {
177
+ this.trackOpenPart(frame.body);
178
+ return frame.body?.trim() ? [{
179
+ ...frame,
180
+ body: ""
181
+ }] : [frame];
182
+ }
183
+ this.ledger.record(this.requestId, frame.seq);
184
+ if (this.openParts.size === 0 || !frame.body?.trim()) return [frame];
185
+ const reopened = [...this.openParts.values()].map((body) => ({
186
+ ...frame,
187
+ body,
188
+ seq: void 0
189
+ }));
190
+ this.openParts.clear();
191
+ return [...reopened, frame];
192
+ }
193
+ trackOpenPart(body) {
194
+ let chunk;
195
+ try {
196
+ chunk = JSON.parse(body ?? "");
197
+ } catch {
198
+ return;
199
+ }
200
+ switch (chunk.type) {
201
+ case "text-start":
202
+ case "reasoning-start":
203
+ this.openParts.set(`${chunk.type}:${chunk.id}`, body ?? "");
204
+ break;
205
+ case "text-end":
206
+ this.openParts.delete(`text-start:${chunk.id}`);
207
+ break;
208
+ case "reasoning-end":
209
+ this.openParts.delete(`reasoning-start:${chunk.id}`);
210
+ break;
211
+ case "tool-input-start":
212
+ this.openParts.set(`tool:${chunk.toolCallId}`, body ?? "");
213
+ break;
214
+ case "tool-input-available":
215
+ case "tool-input-error":
216
+ this.openParts.delete(`tool:${chunk.toolCallId}`);
217
+ break;
218
+ }
219
+ }
220
+ };
221
+ //#endregion
128
222
  //#region src/chat/ws-chat-transport.ts
129
223
  /**
130
224
  * Short safety-net timeout for a resume probe when the server has said nothing.
@@ -141,6 +235,21 @@ const RESUME_PROBE_TIMEOUT_MS = 5e3;
141
235
  * dropped follow-up degrades to a null resolve instead of hanging forever.
142
236
  */
143
237
  const RESUME_PENDING_TIMEOUT_MS = 6e4;
238
+ const SOCKET_CLOSED_MID_STREAM = "WebSocket closed mid-stream";
239
+ /**
240
+ * Ends a stream whose socket closed before the terminal `done` frame, so the
241
+ * AI SDK reports an interrupted turn instead of a completed one (#2013).
242
+ * Uses an error chunk because `controller.error()` would discard chunks the
243
+ * consumer has not read yet.
244
+ */
245
+ function interruptChatStream(controller, batch) {
246
+ batch?.flush();
247
+ controller.enqueue({
248
+ type: "error",
249
+ errorText: SOCKET_CLOSED_MID_STREAM
250
+ });
251
+ controller.close();
252
+ }
144
253
  /**
145
254
  * ChatTransport that sends messages over WebSocket and returns a
146
255
  * ReadableStream<UIMessageChunk> that the AI SDK's useChat consumes directly.
@@ -153,6 +262,7 @@ const RESUME_PENDING_TIMEOUT_MS = 6e4;
153
262
  var WebSocketChatTransport = class {
154
263
  constructor(options) {
155
264
  this._resumeResolver = null;
265
+ this.appliedChunks = new AppliedChunkLedger();
156
266
  this._resumeNoneResolver = null;
157
267
  this._onStreamPending = null;
158
268
  this._retryResumeProbe = null;
@@ -165,6 +275,7 @@ var WebSocketChatTransport = class {
165
275
  this.prepareBody = options.prepareBody;
166
276
  this.activeRequestIds = options.activeRequestIds;
167
277
  this.cancelOnClientAbort = options.cancelOnClientAbort ?? false;
278
+ this.onRequestBuffered = options.onRequestBuffered;
168
279
  }
169
280
  /**
170
281
  * Point the singleton transport at a new Agent connection. A pending resolver
@@ -196,6 +307,14 @@ var WebSocketChatTransport = class {
196
307
  const cancelledToolContinuation = this.abortActiveToolContinuation();
197
308
  return cancelledRequest || cancelledToolContinuation;
198
309
  }
310
+ /**
311
+ * The server turn this transport last attached to that has not reported a
312
+ * terminal frame. A socket close ends the local stream but keeps this set,
313
+ * since the server keeps running the turn.
314
+ */
315
+ get activeServerTurnId() {
316
+ return this._activeServerTurnId;
317
+ }
199
318
  sendCancelFrame(requestId) {
200
319
  try {
201
320
  this.agent.send(JSON.stringify({
@@ -379,7 +498,7 @@ var WebSocketChatTransport = class {
379
498
  } catch {}
380
499
  };
381
500
  const onClose = () => {
382
- finish(() => controller.close(), false, false);
501
+ finish(() => interruptChatStream(controller), false, false);
383
502
  };
384
503
  agent.addEventListener("message", onMessage, { signal: abortController.signal });
385
504
  agent.addEventListener("close", onClose, { signal: abortController.signal });
@@ -394,14 +513,17 @@ var WebSocketChatTransport = class {
394
513
  }
395
514
  if (completed) return stream;
396
515
  requestSent = true;
397
- agent.send(JSON.stringify({
516
+ if (agent.send(JSON.stringify({
398
517
  id: requestId,
399
518
  init: {
400
519
  method: "POST",
401
520
  body: bodyPayload
402
521
  },
403
522
  type: "cf_agent_use_chat_request"
404
- }));
523
+ })) === false && options.trigger === "submit-message") {
524
+ const lastUserMessage = [...options.messages].reverse().find((message) => message.role === "user");
525
+ this.onRequestBuffered?.(lastUserMessage?.id);
526
+ }
405
527
  return stream;
406
528
  }
407
529
  async reconnectToStream(_options) {
@@ -544,6 +666,7 @@ var WebSocketChatTransport = class {
544
666
  readerController = controller;
545
667
  const batch = new ReplayChunkBatch(controller);
546
668
  replayBatch = batch;
669
+ let replayFilter = null;
547
670
  let timeout;
548
671
  const armTimeout = (delay) => {
549
672
  if (timeout) clearTimeout(timeout);
@@ -592,17 +715,19 @@ var WebSocketChatTransport = class {
592
715
  const data = JSON.parse(event.data);
593
716
  if (data.type !== "cf_agent_use_chat_response" || requestId == null || data.id !== requestId) return;
594
717
  if (data.error) {
718
+ transport.appliedChunks.forget(requestId);
595
719
  finish(() => failChatStream(batch, data.body || "Stream error"));
596
720
  return;
597
721
  }
598
- applyChatResponseFrame(batch, data);
599
- if (data.done) finish(() => controller.close());
722
+ replayFilter ??= new ContinuationReplayFilter(transport.appliedChunks, requestId);
723
+ for (const frame of replayFilter.frames(data)) applyChatResponseFrame(batch, frame);
724
+ if (data.done) {
725
+ transport.appliedChunks.forget(requestId);
726
+ finish(() => controller.close());
727
+ }
600
728
  } catch {}
601
729
  };
602
- const onClose = () => finish(() => {
603
- batch.flush();
604
- controller.close();
605
- });
730
+ const onClose = () => finish(() => requestId === null ? controller.close() : interruptChatStream(controller, batch));
606
731
  agent.addEventListener("message", onMessage, { signal: streamController.signal });
607
732
  agent.addEventListener("close", onClose, { signal: streamController.signal });
608
733
  retryResumeProbe();
@@ -661,24 +786,26 @@ var WebSocketChatTransport = class {
661
786
  streamController = controller;
662
787
  const batch = new ReplayChunkBatch(controller);
663
788
  replayBatch = batch;
789
+ const replayFilter = new ContinuationReplayFilter(transport.appliedChunks, requestId);
664
790
  const onMessage = (event) => {
665
791
  try {
666
792
  const data = JSON.parse(event.data);
667
793
  if (data.type !== "cf_agent_use_chat_response") return;
668
794
  if (data.id !== requestId) return;
669
795
  if (data.error) {
796
+ transport.appliedChunks.forget(requestId);
670
797
  finish(() => failChatStream(batch, data.body || "Stream error"));
671
798
  return;
672
799
  }
673
- applyChatResponseFrame(batch, data);
674
- if (data.done) finish(() => controller.close());
800
+ for (const frame of replayFilter.frames(data)) applyChatResponseFrame(batch, frame);
801
+ if (data.done) {
802
+ transport.appliedChunks.forget(requestId);
803
+ finish(() => controller.close());
804
+ }
675
805
  } catch {}
676
806
  };
677
807
  const onClose = () => {
678
- finish(() => {
679
- batch.flush();
680
- controller.close();
681
- }, false, false);
808
+ finish(() => interruptChatStream(controller, batch), false, false);
682
809
  };
683
810
  agent.addEventListener("message", onMessage, { signal: chunkController.signal });
684
811
  agent.addEventListener("close", onClose, { signal: chunkController.signal });
@@ -695,4 +822,4 @@ var WebSocketChatTransport = class {
695
822
  //#endregion
696
823
  export { WebSocketChatTransport as t };
697
824
 
698
- //# sourceMappingURL=ws-chat-transport-rWwta645.js.map
825
+ //# sourceMappingURL=ws-chat-transport-cqMKq3IY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-chat-transport-cqMKq3IY.js","names":[],"sources":["../src/chat/replay-batch.ts","../src/chat/replay-dedupe.ts","../src/chat/ws-chat-transport.ts"],"sourcesContent":["import type { UIMessageChunk } from \"ai\";\n\n/**\n * Batches chunks replayed during stream resume.\n *\n * Replaying every chunk separately can exceed React's update limit. Adjacent\n * deltas for the same message part are merged until the replay pauses or ends.\n */\nexport class ReplayChunkBatch {\n private chunks: UIMessageChunk[] = [];\n private tailMergeKey: string | null = null;\n private cancelWindow: (() => void) | undefined;\n private startMessageId: string | undefined;\n\n constructor(\n private readonly controller: ReadableStreamDefaultController<UIMessageChunk>,\n private readonly closeWindow: (flush: () => void) => () => void = (\n flush\n ) => {\n const timer = setTimeout(flush, 0);\n return () => clearTimeout(timer);\n }\n ) {}\n\n get bufferedStartMessageId(): string | undefined {\n return this.startMessageId;\n }\n\n get isEmpty(): boolean {\n return this.chunks.length === 0;\n }\n\n push(chunk: UIMessageChunk): void {\n const key = mergeKeyOf(chunk);\n const tail = this.chunks[this.chunks.length - 1];\n\n if (tail !== undefined && key !== null && key === this.tailMergeKey) {\n appendDeltaText(tail, chunk);\n return;\n }\n\n if (this.chunks.length === 0) {\n this.cancelWindow = this.closeWindow(() => {\n this.cancelWindow = undefined;\n this.flush();\n });\n }\n\n if (chunk.type === \"start\") {\n this.startMessageId = chunk.messageId;\n }\n this.chunks.push(chunk);\n this.tailMergeKey = key;\n }\n\n discard(): void {\n this.cancelWindow?.();\n this.cancelWindow = undefined;\n this.chunks = [];\n this.tailMergeKey = null;\n this.startMessageId = undefined;\n }\n\n flush(): void {\n this.cancelWindow?.();\n this.cancelWindow = undefined;\n\n if (this.chunks.length === 0) return;\n const batch = this.chunks;\n this.chunks = [];\n this.tailMergeKey = null;\n this.startMessageId = undefined;\n try {\n for (const chunk of batch) {\n this.controller.enqueue(chunk);\n }\n } catch {\n // The stream is already closed; callers still need to finish cleanup.\n }\n }\n\n enqueueNow(chunk: UIMessageChunk): void {\n this.controller.enqueue(chunk);\n }\n\n closeNow(): void {\n this.controller.close();\n }\n\n errorNow(error: Error): void {\n this.controller.error(error);\n }\n}\n\ntype ChatResponseFrame = {\n body?: string;\n continuation?: boolean;\n done?: boolean;\n replay?: boolean;\n replayComplete?: boolean;\n};\n\n/** Buffers replay chunks and flushes them before live or boundary frames. */\nexport function applyChatResponseFrame(\n batch: ReplayChunkBatch,\n frame: ChatResponseFrame\n): void {\n const endsReplayBurst =\n frame.replay !== true ||\n frame.done === true ||\n frame.replayComplete === true;\n\n const body = frame.body?.trim();\n if (!body) {\n if (endsReplayBurst) batch.flush();\n return;\n }\n\n let chunk: UIMessageChunk;\n try {\n chunk = JSON.parse(body) as UIMessageChunk;\n } catch {\n // Do not leave buffered chunks behind a malformed boundary frame.\n if (endsReplayBurst) batch.flush();\n return;\n }\n\n if (endsReplayBurst) {\n batch.flush();\n batch.enqueueNow(chunk);\n return;\n }\n\n if (supersedesBufferedPass(batch, chunk, frame)) {\n batch.discard();\n }\n\n batch.push(chunk);\n}\n\nfunction supersedesBufferedPass(\n batch: ReplayChunkBatch,\n chunk: UIMessageChunk,\n frame: ChatResponseFrame\n): boolean {\n return (\n chunk.type === \"start\" &&\n frame.continuation !== true &&\n chunk.messageId !== undefined &&\n chunk.messageId === batch.bufferedStartMessageId\n );\n}\n\n/**\n * Reports an error after buffered content. `controller.error()` would discard\n * chunks that have not reached the consumer.\n */\nexport function failChatStream(batch: ReplayChunkBatch, message: string): void {\n if (batch.isEmpty) {\n batch.errorNow(new Error(message));\n return;\n }\n\n batch.flush();\n batch.enqueueNow({ errorText: message, type: \"error\" });\n batch.closeNow();\n}\n\nfunction mergeKeyOf(chunk: UIMessageChunk): string | null {\n switch (chunk.type) {\n case \"text-delta\":\n case \"reasoning-delta\":\n return chunk.providerMetadata === undefined\n ? `${chunk.type}\\u0000${chunk.id}`\n : null;\n case \"tool-input-delta\":\n return `tool-input-delta\\u0000${chunk.toolCallId}`;\n default:\n return null;\n }\n}\n\nfunction appendDeltaText(target: UIMessageChunk, source: UIMessageChunk): void {\n if (\n target.type === \"tool-input-delta\" &&\n source.type === \"tool-input-delta\"\n ) {\n target.inputTextDelta += source.inputTextDelta;\n return;\n }\n if (\n (target.type === \"text-delta\" || target.type === \"reasoning-delta\") &&\n (source.type === \"text-delta\" || source.type === \"reasoning-delta\")\n ) {\n target.delta += source.delta;\n }\n}\n","/**\n * Replay de-duplication for continuation streams (#1951).\n *\n * A continuation appends to an assistant message this client may already have\n * rendered live. After a reconnect the server replays the continuation's\n * whole buffer, and applying it on top of the live-rendered bytes would\n * duplicate them. Every live and replayed chunk frame carries its index in\n * the stream (`seq`), so a client that records how far it applied each\n * stream can drop the replayed chunks it already has.\n *\n * Non-continuation replays are left alone: the client resets the message and\n * rebuilds it from chunk 0 for those.\n */\n\ntype ChunkFrame = {\n body?: string;\n continuation?: boolean;\n replay?: boolean;\n seq?: number;\n};\n\n/**\n * Requests the ledger remembers. A request that finishes while the client is\n * disconnected never delivers the terminal frame that would forget it.\n */\nconst MAX_TRACKED_REQUESTS = 32;\n\n/** Highest chunk `seq` this client applied, per request id. */\nexport class AppliedChunkLedger {\n private readonly applied = new Map<string, number>();\n\n record(requestId: string, seq: number | undefined): void {\n if (seq === undefined) return;\n const applied = Math.max(seq, this.applied.get(requestId) ?? -1);\n this.applied.delete(requestId);\n this.applied.set(requestId, applied);\n if (this.applied.size > MAX_TRACKED_REQUESTS) {\n const oldest = this.applied.keys().next().value;\n if (oldest !== undefined) this.applied.delete(oldest);\n }\n }\n\n /** Whether `frame` replays a continuation chunk this client already applied. */\n isAppliedReplay(requestId: string, frame: ChunkFrame): boolean {\n return (\n frame.replay === true &&\n frame.continuation === true &&\n frame.seq !== undefined &&\n frame.seq <= (this.applied.get(requestId) ?? -1)\n );\n }\n\n forget(requestId: string): void {\n this.applied.delete(requestId);\n }\n\n clear(): void {\n this.applied.clear();\n }\n}\n\n/**\n * Filters one resumed stream's frames for a consumer that needs every part\n * opened before its deltas arrive (the AI SDK UI message stream). Applied\n * replay chunks are dropped; a part that was still open when they end is\n * re-opened with its original start chunk, so the new deltas have a part to\n * land in. The re-opened part renders as a second, adjacent part until the\n * server's final message replaces it.\n */\nexport class ContinuationReplayFilter<F extends ChunkFrame> {\n private readonly openParts = new Map<string, string>();\n\n constructor(\n private readonly ledger: AppliedChunkLedger,\n private readonly requestId: string\n ) {}\n\n /** The frames to apply in place of `frame`. */\n frames(frame: F): F[] {\n if (this.ledger.isAppliedReplay(this.requestId, frame)) {\n this.trackOpenPart(frame.body);\n return frame.body?.trim() ? [{ ...frame, body: \"\" }] : [frame];\n }\n this.ledger.record(this.requestId, frame.seq);\n if (this.openParts.size === 0 || !frame.body?.trim()) return [frame];\n const reopened = [...this.openParts.values()].map(\n (body) => ({ ...frame, body, seq: undefined }) as F\n );\n this.openParts.clear();\n return [...reopened, frame];\n }\n\n private trackOpenPart(body: string | undefined): void {\n let chunk: { type?: string; id?: string; toolCallId?: string };\n try {\n chunk = JSON.parse(body ?? \"\") as typeof chunk;\n } catch {\n return;\n }\n switch (chunk.type) {\n case \"text-start\":\n case \"reasoning-start\":\n this.openParts.set(`${chunk.type}:${chunk.id}`, body ?? \"\");\n break;\n case \"text-end\":\n this.openParts.delete(`text-start:${chunk.id}`);\n break;\n case \"reasoning-end\":\n this.openParts.delete(`reasoning-start:${chunk.id}`);\n break;\n case \"tool-input-start\":\n this.openParts.set(`tool:${chunk.toolCallId}`, body ?? \"\");\n break;\n case \"tool-input-available\":\n case \"tool-input-error\":\n this.openParts.delete(`tool:${chunk.toolCallId}`);\n break;\n }\n }\n}\n","/**\n * Framework-neutral WebSocket transport for AI SDK chat clients.\n *\n * Speaks the CF_AGENT protocol directly and returns AI SDK UI message chunks\n * without reconstructing an HTTP response or parsing an SSE stream.\n */\n\nimport type { ChatTransport, UIMessage, UIMessageChunk } from \"ai\";\nimport { nanoid } from \"nanoid\";\nimport {\n applyChatResponseFrame,\n failChatStream,\n ReplayChunkBatch\n} from \"./replay-batch\";\nimport { AppliedChunkLedger, ContinuationReplayFilter } from \"./replay-dedupe\";\nimport { MessageType, type OutgoingMessage } from \"./wire-types\";\n\n/**\n * Short safety-net timeout for a resume probe when the server has said nothing.\n * Under normal operation the server answers a `STREAM_RESUME_REQUEST` with\n * `STREAM_RESUMING`, `STREAM_RESUME_NONE`, or `STREAM_PENDING` well before this.\n */\nconst RESUME_PROBE_TIMEOUT_MS = 5000;\n\n/**\n * Extended backstop applied once the server says a turn is pending\n * (`STREAM_PENDING`, #1784). The pre-stream window (queueing, MCP setup,\n * debounce, model latency) can exceed the short probe timeout, and the server\n * guarantees a follow-up `STREAM_RESUMING` or `STREAM_RESUME_NONE` — so we wait\n * much longer (refreshed on every keep-waiting frame) but still cap it so a\n * dropped follow-up degrades to a null resolve instead of hanging forever.\n */\nconst RESUME_PENDING_TIMEOUT_MS = 60000;\n\nconst SOCKET_CLOSED_MID_STREAM = \"WebSocket closed mid-stream\";\n\n/**\n * Ends a stream whose socket closed before the terminal `done` frame, so the\n * AI SDK reports an interrupted turn instead of a completed one (#2013).\n * Uses an error chunk because `controller.error()` would discard chunks the\n * consumer has not read yet.\n */\nfunction interruptChatStream(\n controller: ReadableStreamDefaultController<UIMessageChunk>,\n batch?: ReplayChunkBatch\n): void {\n batch?.flush();\n controller.enqueue({ type: \"error\", errorText: SOCKET_CLOSED_MID_STREAM });\n controller.close();\n}\n\n/**\n * Agent-like interface for sending/receiving WebSocket messages.\n * Matches `AgentClient` from `agents/client` and the connection returned by\n * `useAgent` from `agents/react`.\n */\nexport interface AgentConnection {\n // PartySocket returns `false` when the frame was buffered (socket not OPEN)\n // rather than sent immediately; `void` keeps non-PartySocket providers valid.\n send: (data: string) => boolean | void;\n addEventListener: (\n type: string,\n listener: (event: MessageEvent) => void,\n options?: { signal?: AbortSignal }\n ) => void;\n removeEventListener: (\n type: string,\n listener: (event: MessageEvent) => void\n ) => void;\n}\n\nexport type WebSocketChatTransportOptions<\n ChatMessage extends UIMessage = UIMessage\n> = {\n /** The framework-neutral AgentClient or useAgent connection. */\n agent: AgentConnection;\n /**\n * Callback to add custom fields to the request body before sending.\n */\n prepareBody?: (options: {\n messages: ChatMessage[];\n trigger: \"submit-message\" | \"regenerate-message\";\n messageId?: string;\n }) => Promise<Record<string, unknown>> | Record<string, unknown>;\n /**\n * Optional set to track active request IDs.\n * IDs are added when a request starts and removed when it completes.\n * Used by the onAgentMessage handler to skip messages already handled by the transport.\n */\n activeRequestIds?: Set<string>;\n /**\n * Whether generic client-side abort/cancel lifecycle should cancel the\n * server turn. Explicit cancellation via cancelActiveServerTurn() always\n * sends CF_AGENT_CHAT_REQUEST_CANCEL.\n * @default false\n */\n cancelOnClientAbort?: boolean;\n /**\n * Called when a `submit-message` request frame was buffered rather than sent\n * immediately (the socket was not OPEN, so PartySocket queued it for the next\n * reconnect). The hook uses this to preserve the optimistic message across the\n * reconnect transcript replay. `messageId` is the submitted user message's id.\n */\n onRequestBuffered?: (messageId: string | undefined) => void;\n};\n\n/**\n * ChatTransport that sends messages over WebSocket and returns a\n * ReadableStream<UIMessageChunk> that the AI SDK's useChat consumes directly.\n *\n * This low-level transport handles new and regenerated request streams plus\n * cancellation. Higher-level protocol coordination such as automatic reconnect\n * resume, cross-tab transcript synchronization, and client-tool continuations\n * is provided by integrations such as `useAgentChat`.\n */\nexport class WebSocketChatTransport<\n ChatMessage extends UIMessage = UIMessage\n> implements ChatTransport<ChatMessage> {\n agent: AgentConnection;\n private prepareBody?: WebSocketChatTransportOptions<ChatMessage>[\"prepareBody\"];\n private activeRequestIds?: Set<string>;\n private cancelOnClientAbort: boolean;\n private onRequestBuffered?: WebSocketChatTransportOptions<ChatMessage>[\"onRequestBuffered\"];\n\n // Pending resume resolver — set by reconnectToStream, called by\n // handleStreamResuming when onAgentMessage sees CF_AGENT_STREAM_RESUMING.\n private _resumeResolver: ((data: { id: string }) => void) | null = null;\n /**\n * Continuation chunks this client applied, shared with the hook's fallback\n * observer so a replay after reconnect skips them on either path (#1951).\n */\n readonly appliedChunks = new AppliedChunkLedger();\n // Pending \"no stream\" resolver — called by handleStreamResumeNone\n // when onAgentMessage sees CF_AGENT_STREAM_RESUME_NONE.\n private _resumeNoneResolver:\n | ((data: { probeId?: string }) => boolean)\n | null = null;\n // Keep-waiting hook (#1784) — set by whichever resume path is currently\n // awaiting, called by handleStreamPending when onAgentMessage sees\n // CF_AGENT_STREAM_PENDING. Extends the path's probe timeout so a slow\n // pre-stream window (queue / MCP / model latency) does not resolve early.\n private _onStreamPending: (() => void) | null = null;\n // Retransmits the current handshake on a replacement socket without starting\n // a second AI SDK resume request. Set only while a resolver is active.\n private _retryResumeProbe: (() => void) | null = null;\n // Set when a client-side tool result/approval is expected to trigger\n // a new continuation stream. In this mode reconnectToStream() returns\n // a deferred ReadableStream immediately so AI SDK status can transition\n // to \"submitted\" before the server starts streaming.\n private _expectToolContinuation = false;\n private _abortToolContinuation: (() => boolean) | null = null;\n private _activeServerTurnId: string | null = null;\n private _cancelAttachedStream: (() => boolean) | null = null;\n // Local-only detach for a resume stream owned by an obsolete Chat/agent\n // generation. Unlike explicit cancellation, this never cancels server work.\n private _detachResumeStream: (() => boolean) | null = null;\n\n constructor(options: WebSocketChatTransportOptions<ChatMessage>) {\n this.agent = options.agent;\n this.prepareBody = options.prepareBody;\n this.activeRequestIds = options.activeRequestIds;\n this.cancelOnClientAbort = options.cancelOnClientAbort ?? false;\n this.onRequestBuffered = options.onRequestBuffered;\n }\n\n /**\n * Point the singleton transport at a new Agent connection. A pending resolver\n * belongs to the old Chat/socket generation and must settle before messages\n * from the replacement connection can be consumed (#1914 review).\n */\n setAgent(agent: AgentConnection) {\n if (this.agent === agent) return;\n this.resetResumeState();\n this.agent = agent;\n }\n\n setCancelOnClientAbort(cancelOnClientAbort: boolean) {\n this.cancelOnClientAbort = cancelOnClientAbort;\n }\n\n /**\n * Explicitly cancel the active server turn, if any.\n * This is separate from generic client-side abort/cancel lifecycle so\n * clients can detach locally without stopping server work.\n */\n cancelActiveServerTurn(): boolean {\n const requestId = this._activeServerTurnId;\n let cancelledRequest = false;\n\n if (requestId) {\n this.sendCancelFrame(requestId);\n this._cancelAttachedStream?.();\n this.clearActiveServerTurn(requestId);\n cancelledRequest = true;\n }\n\n const cancelledToolContinuation = this.abortActiveToolContinuation();\n return cancelledRequest || cancelledToolContinuation;\n }\n\n /**\n * The server turn this transport last attached to that has not reported a\n * terminal frame. A socket close ends the local stream but keeps this set,\n * since the server keeps running the turn.\n */\n get activeServerTurnId(): string | null {\n return this._activeServerTurnId;\n }\n\n private sendCancelFrame(requestId: string) {\n try {\n this.agent.send(\n JSON.stringify({\n id: requestId,\n type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL\n })\n );\n } catch {\n // Ignore failures (e.g. agent already disconnected)\n }\n }\n\n private setActiveServerTurn(\n requestId: string,\n cancelAttachedStream: (() => boolean) | null\n ) {\n this._activeServerTurnId = requestId;\n this._cancelAttachedStream = cancelAttachedStream;\n }\n\n private clearActiveServerTurn(requestId: string) {\n if (this._activeServerTurnId === requestId) {\n this._activeServerTurnId = null;\n this._cancelAttachedStream = null;\n }\n }\n\n /**\n * Mark that the next reconnectToStream() call should attach to a\n * server-initiated tool continuation rather than a page-load resume.\n */\n expectToolContinuation() {\n this._expectToolContinuation = true;\n }\n\n /**\n * Abort the active client-side tool continuation stream, if one is attached\n * to a server request id.\n */\n abortActiveToolContinuation(): boolean {\n return this._abortToolContinuation?.() ?? false;\n }\n\n /**\n * True when the transport is waiting for a resume handshake.\n */\n isAwaitingResume(): boolean {\n return this._resumeResolver !== null || this._resumeNoneResolver !== null;\n }\n\n /**\n * Settle and detach the current handshake without interpreting it as a\n * server-idle response. Used when the owning hook/agent generation changes.\n */\n cancelPendingResume(): boolean {\n const resolveNone = this._resumeNoneResolver;\n if (!resolveNone) return false;\n return resolveNone({});\n }\n\n /**\n * Invalidate all client-side resume state for an obsolete hook/agent\n * generation without cancelling its durable server turn.\n */\n resetResumeState(): void {\n this._expectToolContinuation = false;\n this.cancelPendingResume();\n this._detachResumeStream?.();\n }\n\n /**\n * Re-send the active handshake request on the latest socket generation. This\n * preserves one AI SDK resume operation while recovering a request/reply lost\n * with the previous WebSocket.\n */\n retryPendingResume(): boolean {\n const retry = this._retryResumeProbe;\n if (!retry) return false;\n retry();\n return true;\n }\n\n /**\n * Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUMING.\n * If reconnectToStream is waiting, this handles the resume handshake\n * (ACK + stream creation) and returns true. Otherwise returns false\n * so the caller can use its own fallback path.\n */\n handleStreamResuming(data: { id: string }): boolean {\n if (!this._resumeResolver) return false;\n this._resumeResolver(data);\n return true;\n }\n\n /**\n * Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUME_NONE.\n * If reconnectToStream is waiting, resolves the promise with null\n * immediately (no 5-second timeout). Returns true if handled.\n */\n handleStreamResumeNone(data: { probeId?: string } = {}): boolean {\n if (!this._resumeNoneResolver) return false;\n return this._resumeNoneResolver(data);\n }\n\n /**\n * Called by onAgentMessage when it receives CF_AGENT_STREAM_PENDING (#1784):\n * the server accepted a turn but its stream has not started yet. If a resume\n * path is awaiting, extend its probe timeout (so it keeps waiting for the\n * eventual STREAM_RESUMING / STREAM_RESUME_NONE instead of resolving null\n * after the short window). Returns true if a waiting path consumed it.\n */\n handleStreamPending(): boolean {\n if (!this._onStreamPending) return false;\n this._onStreamPending();\n return true;\n }\n\n /**\n * Called by the hook's shared message handler when a server turn finishes\n * outside the currently attached transport stream, such as after local-only\n * client cleanup.\n */\n handleServerTurnCompleted(requestId: string) {\n this.clearActiveServerTurn(requestId);\n }\n\n /**\n * Register a server turn that is being rendered outside a transport-owned\n * stream, such as the hook's fallback cross-tab/resume observer path.\n */\n observeServerTurn(requestId: string) {\n this.setActiveServerTurn(requestId, null);\n }\n\n async sendMessages(options: {\n chatId: string;\n messages: ChatMessage[];\n abortSignal: AbortSignal | undefined;\n trigger: \"submit-message\" | \"regenerate-message\";\n messageId?: string;\n body?: object;\n headers?: Record<string, string> | Headers;\n metadata?: unknown;\n }): Promise<ReadableStream<UIMessageChunk>> {\n const requestId = nanoid(8);\n const abortController = new AbortController();\n let completed = false;\n let requestSent = false;\n\n // Build the request body\n let extraBody: Record<string, unknown> = {};\n if (this.prepareBody) {\n extraBody = await this.prepareBody({\n messages: options.messages,\n trigger: options.trigger,\n messageId: options.messageId\n });\n }\n if (options.body) {\n extraBody = {\n ...extraBody,\n ...(options.body as Record<string, unknown>)\n };\n }\n\n const bodyPayload = JSON.stringify({\n messages: options.messages,\n trigger: options.trigger,\n ...extraBody\n });\n\n // Track this request so the onAgentMessage handler skips it\n this.activeRequestIds?.add(requestId);\n\n // Create a ReadableStream<UIMessageChunk> that emits parsed chunks\n // as they arrive over the WebSocket\n const agent = this.agent;\n const activeIds = this.activeRequestIds;\n\n // Single cleanup helper — every terminal path (done, error, abort)\n // goes through here exactly once.\n // keepId: when true, do NOT remove requestId from activeIds. Used by\n // explicit cancellation so onAgentMessage skips in-flight chunks\n // and the server's final done:true signal until cleanup happens there.\n const finish = (\n action: () => void,\n keepId = false,\n clearServerTurn = true\n ) => {\n if (completed) return;\n completed = true;\n if (clearServerTurn) {\n this.clearActiveServerTurn(requestId);\n }\n try {\n action();\n } catch {\n // Stream may already be closed\n }\n if (!keepId) {\n activeIds?.delete(requestId);\n }\n abortController.abort();\n };\n\n const abortError = new Error(\"Aborted\");\n abortError.name = \"AbortError\";\n\n const cancelActiveRequest = () => {\n if (completed) return false;\n finish(() => streamController.error(abortError), true);\n return true;\n };\n this.setActiveServerTurn(requestId, cancelActiveRequest);\n\n // Abort handler: terminate the local stream. By default, generic AI SDK\n // abort/cancel lifecycle is local-only so durable server turns can continue\n // and be resumed. Use cancelActiveServerTurn() for explicit user/app\n // cancellation, or cancelOnClientAbort for request-lifetime semantics.\n const onAbort = () => {\n if (completed) return;\n if (this.cancelOnClientAbort) {\n if (requestSent) {\n this.sendCancelFrame(requestId);\n }\n finish(() => streamController.error(abortError), requestSent);\n } else {\n finish(() => streamController.error(abortError), false, !requestSent);\n }\n };\n\n // streamController is assigned synchronously by start(), so it is\n // always available by the time onAbort or onMessage can fire.\n let streamController!: ReadableStreamDefaultController<UIMessageChunk>;\n\n const stream = new ReadableStream<UIMessageChunk>({\n start(controller) {\n streamController = controller;\n\n const onMessage = (event: MessageEvent) => {\n try {\n const data = JSON.parse(\n event.data as string\n ) as OutgoingMessage<ChatMessage>;\n\n if (data.type !== MessageType.CF_AGENT_USE_CHAT_RESPONSE) return;\n if (data.id !== requestId) return;\n\n if (data.error) {\n finish(() =>\n controller.error(new Error(data.body || \"Stream error\"))\n );\n return;\n }\n\n // Parse the body as UIMessageChunk and enqueue\n if (data.body?.trim()) {\n try {\n const chunk = JSON.parse(data.body) as UIMessageChunk;\n controller.enqueue(chunk);\n } catch {\n // Skip malformed chunk bodies\n }\n }\n\n if (data.done) {\n finish(() => controller.close());\n }\n } catch {\n // Ignore non-JSON messages\n }\n };\n\n const onClose = () => {\n finish(() => interruptChatStream(controller), false, false);\n };\n\n agent.addEventListener(\"message\", onMessage, {\n signal: abortController.signal\n });\n agent.addEventListener(\"close\", onClose, {\n signal: abortController.signal\n });\n },\n cancel() {\n onAbort();\n }\n });\n\n // Handle abort from the caller\n if (options.abortSignal) {\n options.abortSignal.addEventListener(\"abort\", onAbort, { once: true });\n if (options.abortSignal.aborted) onAbort();\n }\n\n if (completed) {\n return stream;\n }\n\n // Send the request over WebSocket. `send()` returns false when the frame\n // was buffered (socket not OPEN) rather than sent immediately; a buffered\n // submit means the server hasn't seen the message yet, so tell the hook to\n // preserve it across the reconnect transcript replay. Only an explicit\n // false counts — non-PartySocket doubles return undefined (treated as sent).\n requestSent = true;\n const sent = agent.send(\n JSON.stringify({\n id: requestId,\n init: {\n method: \"POST\",\n body: bodyPayload\n },\n type: MessageType.CF_AGENT_USE_CHAT_REQUEST\n })\n );\n if (sent === false && options.trigger === \"submit-message\") {\n const lastUserMessage = [...options.messages]\n .reverse()\n .find((message) => message.role === \"user\");\n this.onRequestBuffered?.(lastUserMessage?.id);\n }\n\n return stream;\n }\n\n async reconnectToStream(_options: {\n chatId: string;\n }): Promise<ReadableStream<UIMessageChunk> | null> {\n // A transport has one handshake slot. Returning null for an unexpected\n // concurrent caller is safer than overwriting callbacks owned by the first\n // AI SDK request (StrictMode/manual overlap); the hook serializes its normal\n // mount, tool, public, and reconnect entry points.\n if (this.isAwaitingResume()) return null;\n\n if (this._expectToolContinuation) {\n this._expectToolContinuation = false;\n return this._createToolContinuationStream();\n }\n\n // Detect whether the server has an active stream for this chat.\n // Instead of registering another message listener (which races with the\n // hook), expose identity-owned callbacks consumed synchronously by the\n // shared handler.\n const activeIds = this.activeRequestIds;\n const probeId = nanoid(8);\n\n return new Promise<ReadableStream<UIMessageChunk> | null>((resolve) => {\n let resolved = false;\n let timeout: ReturnType<typeof setTimeout> | undefined;\n let resumeResolver: ((data: { id: string }) => void) | null = null;\n let resumeNoneResolver: ((data: { probeId?: string }) => boolean) | null =\n null;\n let onStreamPending: (() => void) | null = null;\n let retryResumeProbe: (() => void) | null = null;\n\n const clearOwnedCallbacks = () => {\n if (resumeResolver && this._resumeResolver === resumeResolver) {\n this._resumeResolver = null;\n }\n if (\n resumeNoneResolver &&\n this._resumeNoneResolver === resumeNoneResolver\n ) {\n this._resumeNoneResolver = null;\n }\n if (onStreamPending && this._onStreamPending === onStreamPending) {\n this._onStreamPending = null;\n }\n if (retryResumeProbe && this._retryResumeProbe === retryResumeProbe) {\n this._retryResumeProbe = null;\n }\n };\n\n const done = (value: ReadableStream<UIMessageChunk> | null) => {\n if (resolved) return;\n resolved = true;\n clearOwnedCallbacks();\n if (timeout) clearTimeout(timeout);\n resolve(value);\n };\n\n const armTimeout = (delay: number) => {\n if (timeout) clearTimeout(timeout);\n timeout = setTimeout(() => done(null), delay);\n };\n\n // Keep-waiting (#1784): the server says a turn is accepted but its stream\n // has not started. Extend this operation's own timeout only.\n onStreamPending = () => {\n if (resolved) return;\n armTimeout(RESUME_PENDING_TIMEOUT_MS);\n };\n\n resumeNoneResolver = (data) => {\n if (data.probeId && data.probeId !== probeId) return false;\n done(null);\n return true;\n };\n\n resumeResolver = (data: { id: string }) => {\n const requestId = data.id;\n activeIds?.add(requestId);\n const stream = this._createResumeStream(requestId);\n\n this.agent.send(\n JSON.stringify({\n type: MessageType.CF_AGENT_STREAM_RESUME_ACK,\n id: requestId\n })\n );\n\n done(stream);\n };\n\n // Re-arm both the request and timeout on each replacement socket. This is\n // a retransmission of this resolver's handshake, not a second Chat resume.\n retryResumeProbe = () => {\n if (resolved) return;\n armTimeout(RESUME_PROBE_TIMEOUT_MS);\n try {\n this.agent.send(\n JSON.stringify({\n type: MessageType.CF_AGENT_STREAM_RESUME_REQUEST,\n probeId\n })\n );\n } catch {\n // The next socket open retries again; the timeout remains a backstop.\n }\n };\n\n this._onStreamPending = onStreamPending;\n this._resumeNoneResolver = resumeNoneResolver;\n this._resumeResolver = resumeResolver;\n this._retryResumeProbe = retryResumeProbe;\n retryResumeProbe();\n });\n }\n\n /**\n * Creates a deferred ReadableStream for client-side tool continuations.\n * The stream is returned immediately so AI SDK status becomes \"submitted\"\n * right after addToolOutput()/addToolApprovalResponse(), then it waits for\n * the server to announce the continuation via STREAM_RESUMING.\n */\n private _createToolContinuationStream(): ReadableStream<UIMessageChunk> {\n const agent = this.agent;\n const activeIds = this.activeRequestIds;\n const streamController = new AbortController();\n const abortError = new Error(\"Aborted\");\n abortError.name = \"AbortError\";\n let completed = false;\n let requestId: string | null = null;\n let readerController: ReadableStreamDefaultController<UIMessageChunk> | null =\n null;\n // Held outside start() so the close and detach paths can flush replayed\n // content before they close the stream.\n let replayBatch: ReplayChunkBatch | null = null;\n const probeId = nanoid(8);\n let onResumeRef: ((data: { id: string }) => void) | null = null;\n let onResumeNoneRef: ((data: { probeId?: string }) => boolean) | null =\n null;\n let onStreamPendingRef: (() => void) | null = null;\n let retryResumeProbeRef: (() => void) | null = null;\n let abortToolContinuationRef: (() => boolean) | null = null;\n let detachResumeStreamRef: (() => boolean) | null = null;\n\n const clearOwnedHandshake = () => {\n if (onResumeRef && this._resumeResolver === onResumeRef) {\n this._resumeResolver = null;\n }\n if (onResumeNoneRef && this._resumeNoneResolver === onResumeNoneRef) {\n this._resumeNoneResolver = null;\n }\n if (onStreamPendingRef && this._onStreamPending === onStreamPendingRef) {\n this._onStreamPending = null;\n }\n if (\n retryResumeProbeRef &&\n this._retryResumeProbe === retryResumeProbeRef\n ) {\n this._retryResumeProbe = null;\n }\n };\n\n const finish = (action: () => void, keepRequestId = false) => {\n if (completed) return;\n completed = true;\n if (this._abortToolContinuation === abortToolContinuationRef) {\n this._abortToolContinuation = null;\n }\n if (this._detachResumeStream === detachResumeStreamRef) {\n this._detachResumeStream = null;\n }\n clearOwnedHandshake();\n try {\n action();\n } catch {\n // Stream may already be closed\n }\n if (requestId && !keepRequestId) {\n activeIds?.delete(requestId);\n }\n streamController.abort();\n };\n\n const transport = this;\n\n abortToolContinuationRef = () => {\n if (completed) return false;\n\n if (requestId === null) {\n finish(() => readerController?.error(abortError));\n return true;\n }\n\n try {\n agent.send(\n JSON.stringify({\n type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL,\n id: requestId\n })\n );\n } catch {\n // Ignore failures (e.g. agent already disconnected)\n }\n\n // Keep the ID so the shared message handler ignores in-flight chunks\n // until the server's terminal frame performs its normal cleanup.\n finish(() => readerController?.error(abortError), true);\n return true;\n };\n this._abortToolContinuation = abortToolContinuationRef;\n detachResumeStreamRef = () => {\n if (completed) return false;\n finish(() => {\n replayBatch?.flush();\n readerController?.close();\n });\n return true;\n };\n this._detachResumeStream = detachResumeStreamRef;\n\n return new ReadableStream<UIMessageChunk>({\n start(controller) {\n readerController = controller;\n const batch = new ReplayChunkBatch(controller);\n replayBatch = batch;\n let replayFilter: ContinuationReplayFilter<\n OutgoingMessage<UIMessage> & {\n type: MessageType.CF_AGENT_USE_CHAT_RESPONSE;\n }\n > | null = null;\n let timeout: ReturnType<typeof setTimeout> | undefined;\n\n const armTimeout = (delay: number) => {\n if (timeout) clearTimeout(timeout);\n timeout = setTimeout(() => finish(() => controller.close()), delay);\n };\n\n const onResumeNone = (data: { probeId?: string }) => {\n if (data.probeId && data.probeId !== probeId) return false;\n finish(() => controller.close());\n return true;\n };\n\n const onResume = (data: { id: string }) => {\n if (requestId) return;\n\n requestId = data.id;\n activeIds?.add(requestId);\n clearOwnedHandshake();\n if (timeout) clearTimeout(timeout);\n\n agent.send(\n JSON.stringify({\n type: MessageType.CF_AGENT_STREAM_RESUME_ACK,\n id: requestId\n })\n );\n };\n\n const onStreamPending = () => {\n if (completed) return;\n armTimeout(RESUME_PENDING_TIMEOUT_MS);\n };\n\n const retryResumeProbe = () => {\n if (completed || requestId !== null) return;\n armTimeout(RESUME_PROBE_TIMEOUT_MS);\n try {\n transport.agent.send(\n JSON.stringify({\n type: MessageType.CF_AGENT_STREAM_RESUME_REQUEST,\n probeId\n })\n );\n } catch {\n // A later socket open can retry this same deferred handshake.\n }\n };\n\n onResumeRef = onResume;\n onResumeNoneRef = onResumeNone;\n onStreamPendingRef = onStreamPending;\n retryResumeProbeRef = retryResumeProbe;\n transport._resumeResolver = onResume;\n transport._resumeNoneResolver = onResumeNone;\n transport._onStreamPending = onStreamPending;\n transport._retryResumeProbe = retryResumeProbe;\n\n const onMessage = (event: MessageEvent) => {\n try {\n const data = JSON.parse(\n event.data as string\n ) as OutgoingMessage<UIMessage>;\n\n if (\n data.type !== MessageType.CF_AGENT_USE_CHAT_RESPONSE ||\n requestId == null ||\n data.id !== requestId\n ) {\n return;\n }\n\n if (data.error) {\n transport.appliedChunks.forget(requestId);\n finish(() => failChatStream(batch, data.body || \"Stream error\"));\n return;\n }\n\n replayFilter ??= new ContinuationReplayFilter(\n transport.appliedChunks,\n requestId\n );\n for (const frame of replayFilter.frames(data)) {\n applyChatResponseFrame(batch, frame);\n }\n\n if (data.done) {\n transport.appliedChunks.forget(requestId);\n finish(() => controller.close());\n }\n } catch {\n // Ignore non-JSON messages\n }\n };\n\n // Before the resume handshake there is no server turn to interrupt;\n // closing cleanly lets the hook re-probe on the next open.\n const onClose = () =>\n finish(() =>\n requestId === null\n ? controller.close()\n : interruptChatStream(controller, batch)\n );\n\n agent.addEventListener(\"message\", onMessage, {\n signal: streamController.signal\n });\n agent.addEventListener(\"close\", onClose, {\n signal: streamController.signal\n });\n\n retryResumeProbe();\n },\n cancel() {\n if (requestId && transport.cancelOnClientAbort) {\n transport.sendCancelFrame(requestId);\n finish(() => {}, true);\n } else {\n finish(() => {});\n }\n }\n });\n }\n\n /**\n * Creates a ReadableStream that receives resumed stream chunks\n * and forwards them to useChat as UIMessageChunk objects.\n */\n private _createResumeStream(\n requestId: string\n ): ReadableStream<UIMessageChunk> {\n // Read agent at resolve time (not when reconnectToStream was called)\n // so chunk listener attaches to the latest socket after _pk changes.\n const agent = this.agent;\n const activeIds = this.activeRequestIds;\n const chunkController = new AbortController();\n const abortError = new Error(\"Aborted\");\n abortError.name = \"AbortError\";\n let completed = false;\n let detachResumeStream: (() => boolean) | null = null;\n\n const finish = (\n action: () => void,\n keepId = false,\n clearServerTurn = true\n ) => {\n if (completed) return;\n completed = true;\n if (clearServerTurn) {\n this.clearActiveServerTurn(requestId);\n }\n if (this._detachResumeStream === detachResumeStream) {\n this._detachResumeStream = null;\n }\n try {\n action();\n } catch {\n // Stream may already be closed\n }\n if (!keepId) {\n activeIds?.delete(requestId);\n }\n chunkController.abort();\n };\n\n let streamController: ReadableStreamDefaultController<UIMessageChunk> | null =\n null;\n // Held outside start() so the close and detach paths can flush replayed\n // content before they close the stream.\n let replayBatch: ReplayChunkBatch | null = null;\n const cancelActiveRequest = () => {\n if (completed) return false;\n finish(() => streamController?.error(abortError), true);\n return true;\n };\n this.setActiveServerTurn(requestId, cancelActiveRequest);\n\n const transport = this;\n detachResumeStream = () => {\n if (completed) return false;\n finish(() => {\n replayBatch?.flush();\n streamController?.close();\n });\n return true;\n };\n this._detachResumeStream = detachResumeStream;\n\n return new ReadableStream<UIMessageChunk>({\n start(controller) {\n streamController = controller;\n const batch = new ReplayChunkBatch(controller);\n replayBatch = batch;\n const replayFilter = new ContinuationReplayFilter<\n OutgoingMessage<UIMessage> & {\n type: MessageType.CF_AGENT_USE_CHAT_RESPONSE;\n }\n >(transport.appliedChunks, requestId);\n\n const onMessage = (event: MessageEvent) => {\n try {\n const data = JSON.parse(\n event.data as string\n ) as OutgoingMessage<UIMessage>;\n\n if (data.type !== MessageType.CF_AGENT_USE_CHAT_RESPONSE) return;\n if (data.id !== requestId) return;\n\n if (data.error) {\n transport.appliedChunks.forget(requestId);\n finish(() => failChatStream(batch, data.body || \"Stream error\"));\n return;\n }\n\n for (const frame of replayFilter.frames(data)) {\n applyChatResponseFrame(batch, frame);\n }\n\n if (data.done) {\n transport.appliedChunks.forget(requestId);\n finish(() => controller.close());\n }\n } catch {\n // Ignore non-JSON messages\n }\n };\n\n const onClose = () => {\n finish(() => interruptChatStream(controller, batch), false, false);\n };\n\n agent.addEventListener(\"message\", onMessage, {\n signal: chunkController.signal\n });\n agent.addEventListener(\"close\", onClose, {\n signal: chunkController.signal\n });\n },\n cancel() {\n if (transport.cancelOnClientAbort) {\n transport.sendCancelFrame(requestId);\n finish(() => {}, true);\n } else {\n finish(() => {}, false, false);\n }\n }\n });\n }\n}\n"],"mappings":";;;;;;;;;AAQA,IAAa,mBAAb,MAA8B;CAM5B,YACE,YACA,eACE,UACG;EACH,MAAM,QAAQ,WAAW,OAAO,CAAC;EACjC,aAAa,aAAa,KAAK;CACjC,GACA;EAPiB,KAAA,aAAA;EACA,KAAA,cAAA;EAPnB,KAAQ,SAA2B,CAAC;EACpC,KAAQ,eAA8B;CAYnC;CAEH,IAAI,yBAA6C;EAC/C,OAAO,KAAK;CACd;CAEA,IAAI,UAAmB;EACrB,OAAO,KAAK,OAAO,WAAW;CAChC;CAEA,KAAK,OAA6B;EAChC,MAAM,MAAM,WAAW,KAAK;EAC5B,MAAM,OAAO,KAAK,OAAO,KAAK,OAAO,SAAS;EAE9C,IAAI,SAAS,KAAA,KAAa,QAAQ,QAAQ,QAAQ,KAAK,cAAc;GACnE,gBAAgB,MAAM,KAAK;GAC3B;EACF;EAEA,IAAI,KAAK,OAAO,WAAW,GACzB,KAAK,eAAe,KAAK,kBAAkB;GACzC,KAAK,eAAe,KAAA;GACpB,KAAK,MAAM;EACb,CAAC;EAGH,IAAI,MAAM,SAAS,SACjB,KAAK,iBAAiB,MAAM;EAE9B,KAAK,OAAO,KAAK,KAAK;EACtB,KAAK,eAAe;CACtB;CAEA,UAAgB;EACd,KAAK,eAAe;EACpB,KAAK,eAAe,KAAA;EACpB,KAAK,SAAS,CAAC;EACf,KAAK,eAAe;EACpB,KAAK,iBAAiB,KAAA;CACxB;CAEA,QAAc;EACZ,KAAK,eAAe;EACpB,KAAK,eAAe,KAAA;EAEpB,IAAI,KAAK,OAAO,WAAW,GAAG;EAC9B,MAAM,QAAQ,KAAK;EACnB,KAAK,SAAS,CAAC;EACf,KAAK,eAAe;EACpB,KAAK,iBAAiB,KAAA;EACtB,IAAI;GACF,KAAK,MAAM,SAAS,OAClB,KAAK,WAAW,QAAQ,KAAK;EAEjC,QAAQ,CAER;CACF;CAEA,WAAW,OAA6B;EACtC,KAAK,WAAW,QAAQ,KAAK;CAC/B;CAEA,WAAiB;EACf,KAAK,WAAW,MAAM;CACxB;CAEA,SAAS,OAAoB;EAC3B,KAAK,WAAW,MAAM,KAAK;CAC7B;AACF;;AAWA,SAAgB,uBACd,OACA,OACM;CACN,MAAM,kBACJ,MAAM,WAAW,QACjB,MAAM,SAAS,QACf,MAAM,mBAAmB;CAE3B,MAAM,OAAO,MAAM,MAAM,KAAK;CAC9B,IAAI,CAAC,MAAM;EACT,IAAI,iBAAiB,MAAM,MAAM;EACjC;CACF;CAEA,IAAI;CACJ,IAAI;EACF,QAAQ,KAAK,MAAM,IAAI;CACzB,QAAQ;EAEN,IAAI,iBAAiB,MAAM,MAAM;EACjC;CACF;CAEA,IAAI,iBAAiB;EACnB,MAAM,MAAM;EACZ,MAAM,WAAW,KAAK;EACtB;CACF;CAEA,IAAI,uBAAuB,OAAO,OAAO,KAAK,GAC5C,MAAM,QAAQ;CAGhB,MAAM,KAAK,KAAK;AAClB;AAEA,SAAS,uBACP,OACA,OACA,OACS;CACT,OACE,MAAM,SAAS,WACf,MAAM,iBAAiB,QACvB,MAAM,cAAc,KAAA,KACpB,MAAM,cAAc,MAAM;AAE9B;;;;;AAMA,SAAgB,eAAe,OAAyB,SAAuB;CAC7E,IAAI,MAAM,SAAS;EACjB,MAAM,SAAS,IAAI,MAAM,OAAO,CAAC;EACjC;CACF;CAEA,MAAM,MAAM;CACZ,MAAM,WAAW;EAAE,WAAW;EAAS,MAAM;CAAQ,CAAC;CACtD,MAAM,SAAS;AACjB;AAEA,SAAS,WAAW,OAAsC;CACxD,QAAQ,MAAM,MAAd;EACE,KAAK;EACL,KAAK,mBACH,OAAO,MAAM,qBAAqB,KAAA,IAC9B,GAAG,MAAM,KAAK,QAAQ,MAAM,OAC5B;EACN,KAAK,oBACH,OAAO,yBAAyB,MAAM;EACxC,SACE,OAAO;CACX;AACF;AAEA,SAAS,gBAAgB,QAAwB,QAA8B;CAC7E,IACE,OAAO,SAAS,sBAChB,OAAO,SAAS,oBAChB;EACA,OAAO,kBAAkB,OAAO;EAChC;CACF;CACA,KACG,OAAO,SAAS,gBAAgB,OAAO,SAAS,uBAChD,OAAO,SAAS,gBAAgB,OAAO,SAAS,oBAEjD,OAAO,SAAS,OAAO;AAE3B;;;;;;;AC3KA,MAAM,uBAAuB;;AAG7B,IAAa,qBAAb,MAAgC;;EAC9B,KAAiB,0BAAU,IAAI,IAAoB;;CAEnD,OAAO,WAAmB,KAA+B;EACvD,IAAI,QAAQ,KAAA,GAAW;EACvB,MAAM,UAAU,KAAK,IAAI,KAAK,KAAK,QAAQ,IAAI,SAAS,KAAK,EAAE;EAC/D,KAAK,QAAQ,OAAO,SAAS;EAC7B,KAAK,QAAQ,IAAI,WAAW,OAAO;EACnC,IAAI,KAAK,QAAQ,OAAO,sBAAsB;GAC5C,MAAM,SAAS,KAAK,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;GAC1C,IAAI,WAAW,KAAA,GAAW,KAAK,QAAQ,OAAO,MAAM;EACtD;CACF;;CAGA,gBAAgB,WAAmB,OAA4B;EAC7D,OACE,MAAM,WAAW,QACjB,MAAM,iBAAiB,QACvB,MAAM,QAAQ,KAAA,KACd,MAAM,QAAQ,KAAK,QAAQ,IAAI,SAAS,KAAK;CAEjD;CAEA,OAAO,WAAyB;EAC9B,KAAK,QAAQ,OAAO,SAAS;CAC/B;CAEA,QAAc;EACZ,KAAK,QAAQ,MAAM;CACrB;AACF;;;;;;;;;AAUA,IAAa,2BAAb,MAA4D;CAG1D,YACE,QACA,WACA;EAFiB,KAAA,SAAA;EACA,KAAA,YAAA;EAJnB,KAAiB,4BAAY,IAAI,IAAoB;CAKlD;;CAGH,OAAO,OAAe;EACpB,IAAI,KAAK,OAAO,gBAAgB,KAAK,WAAW,KAAK,GAAG;GACtD,KAAK,cAAc,MAAM,IAAI;GAC7B,OAAO,MAAM,MAAM,KAAK,IAAI,CAAC;IAAE,GAAG;IAAO,MAAM;GAAG,CAAC,IAAI,CAAC,KAAK;EAC/D;EACA,KAAK,OAAO,OAAO,KAAK,WAAW,MAAM,GAAG;EAC5C,IAAI,KAAK,UAAU,SAAS,KAAK,CAAC,MAAM,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK;EACnE,MAAM,WAAW,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,KAC3C,UAAU;GAAE,GAAG;GAAO;GAAM,KAAK,KAAA;EAAU,EAC9C;EACA,KAAK,UAAU,MAAM;EACrB,OAAO,CAAC,GAAG,UAAU,KAAK;CAC5B;CAEA,cAAsB,MAAgC;EACpD,IAAI;EACJ,IAAI;GACF,QAAQ,KAAK,MAAM,QAAQ,EAAE;EAC/B,QAAQ;GACN;EACF;EACA,QAAQ,MAAM,MAAd;GACE,KAAK;GACL,KAAK;IACH,KAAK,UAAU,IAAI,GAAG,MAAM,KAAK,GAAG,MAAM,MAAM,QAAQ,EAAE;IAC1D;GACF,KAAK;IACH,KAAK,UAAU,OAAO,cAAc,MAAM,IAAI;IAC9C;GACF,KAAK;IACH,KAAK,UAAU,OAAO,mBAAmB,MAAM,IAAI;IACnD;GACF,KAAK;IACH,KAAK,UAAU,IAAI,QAAQ,MAAM,cAAc,QAAQ,EAAE;IACzD;GACF,KAAK;GACL,KAAK;IACH,KAAK,UAAU,OAAO,QAAQ,MAAM,YAAY;IAChD;EACJ;CACF;AACF;;;;;;;;ACjGA,MAAM,0BAA0B;;;;;;;;;AAUhC,MAAM,4BAA4B;AAElC,MAAM,2BAA2B;;;;;;;AAQjC,SAAS,oBACP,YACA,OACM;CACN,OAAO,MAAM;CACb,WAAW,QAAQ;EAAE,MAAM;EAAS,WAAW;CAAyB,CAAC;CACzE,WAAW,MAAM;AACnB;;;;;;;;;;AAkEA,IAAa,yBAAb,MAEwC;CAwCtC,YAAY,SAAqD;EA/BjE,KAAQ,kBAA2D;EAKnE,KAAS,gBAAgB,IAAI,mBAAmB;EAGhD,KAAQ,sBAEG;EAKX,KAAQ,mBAAwC;EAGhD,KAAQ,oBAAyC;EAKjD,KAAQ,0BAA0B;EAClC,KAAQ,yBAAiD;EACzD,KAAQ,sBAAqC;EAC7C,KAAQ,wBAAgD;EAGxD,KAAQ,sBAA8C;EAGpD,KAAK,QAAQ,QAAQ;EACrB,KAAK,cAAc,QAAQ;EAC3B,KAAK,mBAAmB,QAAQ;EAChC,KAAK,sBAAsB,QAAQ,uBAAuB;EAC1D,KAAK,oBAAoB,QAAQ;CACnC;;;;;;CAOA,SAAS,OAAwB;EAC/B,IAAI,KAAK,UAAU,OAAO;EAC1B,KAAK,iBAAiB;EACtB,KAAK,QAAQ;CACf;CAEA,uBAAuB,qBAA8B;EACnD,KAAK,sBAAsB;CAC7B;;;;;;CAOA,yBAAkC;EAChC,MAAM,YAAY,KAAK;EACvB,IAAI,mBAAmB;EAEvB,IAAI,WAAW;GACb,KAAK,gBAAgB,SAAS;GAC9B,KAAK,wBAAwB;GAC7B,KAAK,sBAAsB,SAAS;GACpC,mBAAmB;EACrB;EAEA,MAAM,4BAA4B,KAAK,4BAA4B;EACnE,OAAO,oBAAoB;CAC7B;;;;;;CAOA,IAAI,qBAAoC;EACtC,OAAO,KAAK;CACd;CAEA,gBAAwB,WAAmB;EACzC,IAAI;GACF,KAAK,MAAM,KACT,KAAK,UAAU;IACb,IAAI;IACJ,MAAA;GACF,CAAC,CACH;EACF,QAAQ,CAER;CACF;CAEA,oBACE,WACA,sBACA;EACA,KAAK,sBAAsB;EAC3B,KAAK,wBAAwB;CAC/B;CAEA,sBAA8B,WAAmB;EAC/C,IAAI,KAAK,wBAAwB,WAAW;GAC1C,KAAK,sBAAsB;GAC3B,KAAK,wBAAwB;EAC/B;CACF;;;;;CAMA,yBAAyB;EACvB,KAAK,0BAA0B;CACjC;;;;;CAMA,8BAAuC;EACrC,OAAO,KAAK,yBAAyB,KAAK;CAC5C;;;;CAKA,mBAA4B;EAC1B,OAAO,KAAK,oBAAoB,QAAQ,KAAK,wBAAwB;CACvE;;;;;CAMA,sBAA+B;EAC7B,MAAM,cAAc,KAAK;EACzB,IAAI,CAAC,aAAa,OAAO;EACzB,OAAO,YAAY,CAAC,CAAC;CACvB;;;;;CAMA,mBAAyB;EACvB,KAAK,0BAA0B;EAC/B,KAAK,oBAAoB;EACzB,KAAK,sBAAsB;CAC7B;;;;;;CAOA,qBAA8B;EAC5B,MAAM,QAAQ,KAAK;EACnB,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM;EACN,OAAO;CACT;;;;;;;CAQA,qBAAqB,MAA+B;EAClD,IAAI,CAAC,KAAK,iBAAiB,OAAO;EAClC,KAAK,gBAAgB,IAAI;EACzB,OAAO;CACT;;;;;;CAOA,uBAAuB,OAA6B,CAAC,GAAY;EAC/D,IAAI,CAAC,KAAK,qBAAqB,OAAO;EACtC,OAAO,KAAK,oBAAoB,IAAI;CACtC;;;;;;;;CASA,sBAA+B;EAC7B,IAAI,CAAC,KAAK,kBAAkB,OAAO;EACnC,KAAK,iBAAiB;EACtB,OAAO;CACT;;;;;;CAOA,0BAA0B,WAAmB;EAC3C,KAAK,sBAAsB,SAAS;CACtC;;;;;CAMA,kBAAkB,WAAmB;EACnC,KAAK,oBAAoB,WAAW,IAAI;CAC1C;CAEA,MAAM,aAAa,SASyB;EAC1C,MAAM,YAAY,OAAO,CAAC;EAC1B,MAAM,kBAAkB,IAAI,gBAAgB;EAC5C,IAAI,YAAY;EAChB,IAAI,cAAc;EAGlB,IAAI,YAAqC,CAAC;EAC1C,IAAI,KAAK,aACP,YAAY,MAAM,KAAK,YAAY;GACjC,UAAU,QAAQ;GAClB,SAAS,QAAQ;GACjB,WAAW,QAAQ;EACrB,CAAC;EAEH,IAAI,QAAQ,MACV,YAAY;GACV,GAAG;GACH,GAAI,QAAQ;EACd;EAGF,MAAM,cAAc,KAAK,UAAU;GACjC,UAAU,QAAQ;GAClB,SAAS,QAAQ;GACjB,GAAG;EACL,CAAC;EAGD,KAAK,kBAAkB,IAAI,SAAS;EAIpC,MAAM,QAAQ,KAAK;EACnB,MAAM,YAAY,KAAK;EAOvB,MAAM,UACJ,QACA,SAAS,OACT,kBAAkB,SACf;GACH,IAAI,WAAW;GACf,YAAY;GACZ,IAAI,iBACF,KAAK,sBAAsB,SAAS;GAEtC,IAAI;IACF,OAAO;GACT,QAAQ,CAER;GACA,IAAI,CAAC,QACH,WAAW,OAAO,SAAS;GAE7B,gBAAgB,MAAM;EACxB;EAEA,MAAM,6BAAa,IAAI,MAAM,SAAS;EACtC,WAAW,OAAO;EAElB,MAAM,4BAA4B;GAChC,IAAI,WAAW,OAAO;GACtB,aAAa,iBAAiB,MAAM,UAAU,GAAG,IAAI;GACrD,OAAO;EACT;EACA,KAAK,oBAAoB,WAAW,mBAAmB;EAMvD,MAAM,gBAAgB;GACpB,IAAI,WAAW;GACf,IAAI,KAAK,qBAAqB;IAC5B,IAAI,aACF,KAAK,gBAAgB,SAAS;IAEhC,aAAa,iBAAiB,MAAM,UAAU,GAAG,WAAW;GAC9D,OACE,aAAa,iBAAiB,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW;EAExE;EAIA,IAAI;EAEJ,MAAM,SAAS,IAAI,eAA+B;GAChD,MAAM,YAAY;IAChB,mBAAmB;IAEnB,MAAM,aAAa,UAAwB;KACzC,IAAI;MACF,MAAM,OAAO,KAAK,MAChB,MAAM,IACR;MAEA,IAAI,KAAK,SAAA,8BAAiD;MAC1D,IAAI,KAAK,OAAO,WAAW;MAE3B,IAAI,KAAK,OAAO;OACd,aACE,WAAW,MAAM,IAAI,MAAM,KAAK,QAAQ,cAAc,CAAC,CACzD;OACA;MACF;MAGA,IAAI,KAAK,MAAM,KAAK,GAClB,IAAI;OACF,MAAM,QAAQ,KAAK,MAAM,KAAK,IAAI;OAClC,WAAW,QAAQ,KAAK;MAC1B,QAAQ,CAER;MAGF,IAAI,KAAK,MACP,aAAa,WAAW,MAAM,CAAC;KAEnC,QAAQ,CAER;IACF;IAEA,MAAM,gBAAgB;KACpB,aAAa,oBAAoB,UAAU,GAAG,OAAO,KAAK;IAC5D;IAEA,MAAM,iBAAiB,WAAW,WAAW,EAC3C,QAAQ,gBAAgB,OAC1B,CAAC;IACD,MAAM,iBAAiB,SAAS,SAAS,EACvC,QAAQ,gBAAgB,OAC1B,CAAC;GACH;GACA,SAAS;IACP,QAAQ;GACV;EACF,CAAC;EAGD,IAAI,QAAQ,aAAa;GACvB,QAAQ,YAAY,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;GACrE,IAAI,QAAQ,YAAY,SAAS,QAAQ;EAC3C;EAEA,IAAI,WACF,OAAO;EAQT,cAAc;EAWd,IAVa,MAAM,KACjB,KAAK,UAAU;GACb,IAAI;GACJ,MAAM;IACJ,QAAQ;IACR,MAAM;GACR;GACA,MAAA;EACF,CAAC,CAEI,MAAM,SAAS,QAAQ,YAAY,kBAAkB;GAC1D,MAAM,kBAAkB,CAAC,GAAG,QAAQ,QAAQ,CAAC,CAC1C,QAAQ,CAAC,CACT,MAAM,YAAY,QAAQ,SAAS,MAAM;GAC5C,KAAK,oBAAoB,iBAAiB,EAAE;EAC9C;EAEA,OAAO;CACT;CAEA,MAAM,kBAAkB,UAE2B;EAKjD,IAAI,KAAK,iBAAiB,GAAG,OAAO;EAEpC,IAAI,KAAK,yBAAyB;GAChC,KAAK,0BAA0B;GAC/B,OAAO,KAAK,8BAA8B;EAC5C;EAMA,MAAM,YAAY,KAAK;EACvB,MAAM,UAAU,OAAO,CAAC;EAExB,OAAO,IAAI,SAAgD,YAAY;GACrE,IAAI,WAAW;GACf,IAAI;GACJ,IAAI,iBAA0D;GAC9D,IAAI,qBACF;GACF,IAAI,kBAAuC;GAC3C,IAAI,mBAAwC;GAE5C,MAAM,4BAA4B;IAChC,IAAI,kBAAkB,KAAK,oBAAoB,gBAC7C,KAAK,kBAAkB;IAEzB,IACE,sBACA,KAAK,wBAAwB,oBAE7B,KAAK,sBAAsB;IAE7B,IAAI,mBAAmB,KAAK,qBAAqB,iBAC/C,KAAK,mBAAmB;IAE1B,IAAI,oBAAoB,KAAK,sBAAsB,kBACjD,KAAK,oBAAoB;GAE7B;GAEA,MAAM,QAAQ,UAAiD;IAC7D,IAAI,UAAU;IACd,WAAW;IACX,oBAAoB;IACpB,IAAI,SAAS,aAAa,OAAO;IACjC,QAAQ,KAAK;GACf;GAEA,MAAM,cAAc,UAAkB;IACpC,IAAI,SAAS,aAAa,OAAO;IACjC,UAAU,iBAAiB,KAAK,IAAI,GAAG,KAAK;GAC9C;GAIA,wBAAwB;IACtB,IAAI,UAAU;IACd,WAAW,yBAAyB;GACtC;GAEA,sBAAsB,SAAS;IAC7B,IAAI,KAAK,WAAW,KAAK,YAAY,SAAS,OAAO;IACrD,KAAK,IAAI;IACT,OAAO;GACT;GAEA,kBAAkB,SAAyB;IACzC,MAAM,YAAY,KAAK;IACvB,WAAW,IAAI,SAAS;IACxB,MAAM,SAAS,KAAK,oBAAoB,SAAS;IAEjD,KAAK,MAAM,KACT,KAAK,UAAU;KACb,MAAA;KACA,IAAI;IACN,CAAC,CACH;IAEA,KAAK,MAAM;GACb;GAIA,yBAAyB;IACvB,IAAI,UAAU;IACd,WAAW,uBAAuB;IAClC,IAAI;KACF,KAAK,MAAM,KACT,KAAK,UAAU;MACb,MAAA;MACA;KACF,CAAC,CACH;IACF,QAAQ,CAER;GACF;GAEA,KAAK,mBAAmB;GACxB,KAAK,sBAAsB;GAC3B,KAAK,kBAAkB;GACvB,KAAK,oBAAoB;GACzB,iBAAiB;EACnB,CAAC;CACH;;;;;;;CAQA,gCAAwE;EACtE,MAAM,QAAQ,KAAK;EACnB,MAAM,YAAY,KAAK;EACvB,MAAM,mBAAmB,IAAI,gBAAgB;EAC7C,MAAM,6BAAa,IAAI,MAAM,SAAS;EACtC,WAAW,OAAO;EAClB,IAAI,YAAY;EAChB,IAAI,YAA2B;EAC/B,IAAI,mBACF;EAGF,IAAI,cAAuC;EAC3C,MAAM,UAAU,OAAO,CAAC;EACxB,IAAI,cAAuD;EAC3D,IAAI,kBACF;EACF,IAAI,qBAA0C;EAC9C,IAAI,sBAA2C;EAC/C,IAAI,2BAAmD;EACvD,IAAI,wBAAgD;EAEpD,MAAM,4BAA4B;GAChC,IAAI,eAAe,KAAK,oBAAoB,aAC1C,KAAK,kBAAkB;GAEzB,IAAI,mBAAmB,KAAK,wBAAwB,iBAClD,KAAK,sBAAsB;GAE7B,IAAI,sBAAsB,KAAK,qBAAqB,oBAClD,KAAK,mBAAmB;GAE1B,IACE,uBACA,KAAK,sBAAsB,qBAE3B,KAAK,oBAAoB;EAE7B;EAEA,MAAM,UAAU,QAAoB,gBAAgB,UAAU;GAC5D,IAAI,WAAW;GACf,YAAY;GACZ,IAAI,KAAK,2BAA2B,0BAClC,KAAK,yBAAyB;GAEhC,IAAI,KAAK,wBAAwB,uBAC/B,KAAK,sBAAsB;GAE7B,oBAAoB;GACpB,IAAI;IACF,OAAO;GACT,QAAQ,CAER;GACA,IAAI,aAAa,CAAC,eAChB,WAAW,OAAO,SAAS;GAE7B,iBAAiB,MAAM;EACzB;EAEA,MAAM,YAAY;EAElB,iCAAiC;GAC/B,IAAI,WAAW,OAAO;GAEtB,IAAI,cAAc,MAAM;IACtB,aAAa,kBAAkB,MAAM,UAAU,CAAC;IAChD,OAAO;GACT;GAEA,IAAI;IACF,MAAM,KACJ,KAAK,UAAU;KACb,MAAA;KACA,IAAI;IACN,CAAC,CACH;GACF,QAAQ,CAER;GAIA,aAAa,kBAAkB,MAAM,UAAU,GAAG,IAAI;GACtD,OAAO;EACT;EACA,KAAK,yBAAyB;EAC9B,8BAA8B;GAC5B,IAAI,WAAW,OAAO;GACtB,aAAa;IACX,aAAa,MAAM;IACnB,kBAAkB,MAAM;GAC1B,CAAC;GACD,OAAO;EACT;EACA,KAAK,sBAAsB;EAE3B,OAAO,IAAI,eAA+B;GACxC,MAAM,YAAY;IAChB,mBAAmB;IACnB,MAAM,QAAQ,IAAI,iBAAiB,UAAU;IAC7C,cAAc;IACd,IAAI,eAIO;IACX,IAAI;IAEJ,MAAM,cAAc,UAAkB;KACpC,IAAI,SAAS,aAAa,OAAO;KACjC,UAAU,iBAAiB,aAAa,WAAW,MAAM,CAAC,GAAG,KAAK;IACpE;IAEA,MAAM,gBAAgB,SAA+B;KACnD,IAAI,KAAK,WAAW,KAAK,YAAY,SAAS,OAAO;KACrD,aAAa,WAAW,MAAM,CAAC;KAC/B,OAAO;IACT;IAEA,MAAM,YAAY,SAAyB;KACzC,IAAI,WAAW;KAEf,YAAY,KAAK;KACjB,WAAW,IAAI,SAAS;KACxB,oBAAoB;KACpB,IAAI,SAAS,aAAa,OAAO;KAEjC,MAAM,KACJ,KAAK,UAAU;MACb,MAAA;MACA,IAAI;KACN,CAAC,CACH;IACF;IAEA,MAAM,wBAAwB;KAC5B,IAAI,WAAW;KACf,WAAW,yBAAyB;IACtC;IAEA,MAAM,yBAAyB;KAC7B,IAAI,aAAa,cAAc,MAAM;KACrC,WAAW,uBAAuB;KAClC,IAAI;MACF,UAAU,MAAM,KACd,KAAK,UAAU;OACb,MAAA;OACA;MACF,CAAC,CACH;KACF,QAAQ,CAER;IACF;IAEA,cAAc;IACd,kBAAkB;IAClB,qBAAqB;IACrB,sBAAsB;IACtB,UAAU,kBAAkB;IAC5B,UAAU,sBAAsB;IAChC,UAAU,mBAAmB;IAC7B,UAAU,oBAAoB;IAE9B,MAAM,aAAa,UAAwB;KACzC,IAAI;MACF,MAAM,OAAO,KAAK,MAChB,MAAM,IACR;MAEA,IACE,KAAK,SAAA,gCACL,aAAa,QACb,KAAK,OAAO,WAEZ;MAGF,IAAI,KAAK,OAAO;OACd,UAAU,cAAc,OAAO,SAAS;OACxC,aAAa,eAAe,OAAO,KAAK,QAAQ,cAAc,CAAC;OAC/D;MACF;MAEA,iBAAiB,IAAI,yBACnB,UAAU,eACV,SACF;MACA,KAAK,MAAM,SAAS,aAAa,OAAO,IAAI,GAC1C,uBAAuB,OAAO,KAAK;MAGrC,IAAI,KAAK,MAAM;OACb,UAAU,cAAc,OAAO,SAAS;OACxC,aAAa,WAAW,MAAM,CAAC;MACjC;KACF,QAAQ,CAER;IACF;IAIA,MAAM,gBACJ,aACE,cAAc,OACV,WAAW,MAAM,IACjB,oBAAoB,YAAY,KAAK,CAC3C;IAEF,MAAM,iBAAiB,WAAW,WAAW,EAC3C,QAAQ,iBAAiB,OAC3B,CAAC;IACD,MAAM,iBAAiB,SAAS,SAAS,EACvC,QAAQ,iBAAiB,OAC3B,CAAC;IAED,iBAAiB;GACnB;GACA,SAAS;IACP,IAAI,aAAa,UAAU,qBAAqB;KAC9C,UAAU,gBAAgB,SAAS;KACnC,aAAa,CAAC,GAAG,IAAI;IACvB,OACE,aAAa,CAAC,CAAC;GAEnB;EACF,CAAC;CACH;;;;;CAMA,oBACE,WACgC;EAGhC,MAAM,QAAQ,KAAK;EACnB,MAAM,YAAY,KAAK;EACvB,MAAM,kBAAkB,IAAI,gBAAgB;EAC5C,MAAM,6BAAa,IAAI,MAAM,SAAS;EACtC,WAAW,OAAO;EAClB,IAAI,YAAY;EAChB,IAAI,qBAA6C;EAEjD,MAAM,UACJ,QACA,SAAS,OACT,kBAAkB,SACf;GACH,IAAI,WAAW;GACf,YAAY;GACZ,IAAI,iBACF,KAAK,sBAAsB,SAAS;GAEtC,IAAI,KAAK,wBAAwB,oBAC/B,KAAK,sBAAsB;GAE7B,IAAI;IACF,OAAO;GACT,QAAQ,CAER;GACA,IAAI,CAAC,QACH,WAAW,OAAO,SAAS;GAE7B,gBAAgB,MAAM;EACxB;EAEA,IAAI,mBACF;EAGF,IAAI,cAAuC;EAC3C,MAAM,4BAA4B;GAChC,IAAI,WAAW,OAAO;GACtB,aAAa,kBAAkB,MAAM,UAAU,GAAG,IAAI;GACtD,OAAO;EACT;EACA,KAAK,oBAAoB,WAAW,mBAAmB;EAEvD,MAAM,YAAY;EAClB,2BAA2B;GACzB,IAAI,WAAW,OAAO;GACtB,aAAa;IACX,aAAa,MAAM;IACnB,kBAAkB,MAAM;GAC1B,CAAC;GACD,OAAO;EACT;EACA,KAAK,sBAAsB;EAE3B,OAAO,IAAI,eAA+B;GACxC,MAAM,YAAY;IAChB,mBAAmB;IACnB,MAAM,QAAQ,IAAI,iBAAiB,UAAU;IAC7C,cAAc;IACd,MAAM,eAAe,IAAI,yBAIvB,UAAU,eAAe,SAAS;IAEpC,MAAM,aAAa,UAAwB;KACzC,IAAI;MACF,MAAM,OAAO,KAAK,MAChB,MAAM,IACR;MAEA,IAAI,KAAK,SAAA,8BAAiD;MAC1D,IAAI,KAAK,OAAO,WAAW;MAE3B,IAAI,KAAK,OAAO;OACd,UAAU,cAAc,OAAO,SAAS;OACxC,aAAa,eAAe,OAAO,KAAK,QAAQ,cAAc,CAAC;OAC/D;MACF;MAEA,KAAK,MAAM,SAAS,aAAa,OAAO,IAAI,GAC1C,uBAAuB,OAAO,KAAK;MAGrC,IAAI,KAAK,MAAM;OACb,UAAU,cAAc,OAAO,SAAS;OACxC,aAAa,WAAW,MAAM,CAAC;MACjC;KACF,QAAQ,CAER;IACF;IAEA,MAAM,gBAAgB;KACpB,aAAa,oBAAoB,YAAY,KAAK,GAAG,OAAO,KAAK;IACnE;IAEA,MAAM,iBAAiB,WAAW,WAAW,EAC3C,QAAQ,gBAAgB,OAC1B,CAAC;IACD,MAAM,iBAAiB,SAAS,SAAS,EACvC,QAAQ,gBAAgB,OAC1B,CAAC;GACH;GACA,SAAS;IACP,IAAI,UAAU,qBAAqB;KACjC,UAAU,gBAAgB,SAAS;KACnC,aAAa,CAAC,GAAG,IAAI;IACvB,OACE,aAAa,CAAC,GAAG,OAAO,KAAK;GAEjC;EACF,CAAC;CACH;AACF"}
@@ -460,6 +460,31 @@ bounded only by the absolute `detachedMaxBudgetMs` ceiling — a run is never
460
460
  given up on merely for being slow. Set `noProgressBudgetMs` to `0` or `Infinity`
461
461
  to disable the resetting window for a run.
462
462
 
463
+ ### Deliver only progress and the final result
464
+
465
+ By default, the parent forwards every chunk of the child's stream to its
466
+ clients, and the child broadcasts the same chunks to its own connections. When
467
+ nobody watches the child's text as it streams (for example, a server-side
468
+ pipeline that only reads the result), pass `eventDelivery: "terminal"`:
469
+
470
+ ```ts
471
+ const result = await this.runAgentTool(ResearchAgent, {
472
+ input: { query: "Durable Objects" },
473
+ eventDelivery: "terminal"
474
+ });
475
+ ```
476
+
477
+ The run still produces `started`, progress, milestone, and terminal
478
+ (`finished`, `error`, `aborted`, or `interrupted`) events, and the result,
479
+ summary, and output are unchanged. Ordinary child chunks are not forwarded to the
480
+ parent's clients, are not replayed on reconnect, and are not broadcast by the
481
+ child. The child still stores its own conversation, so `getAgentToolChunks()`
482
+ keeps working. A client that drills in to the child sees the stored
483
+ conversation when it connects and the final messages when the run ends, but
484
+ does not see text stream live while the run is in flight. Use the default
485
+ `eventDelivery` for runs you expect users to watch. Detached runs do not
486
+ support `eventDelivery: "terminal"` and reject it.
487
+
463
488
  ## Render child timelines in React
464
489
 
465
490
  `useAgentToolEvents()` is a headless hook. It subscribes to the existing parent
@@ -543,10 +568,10 @@ finalized in the parent without re-running already-finished work.
543
568
  The re-attach wait is **progress-keyed**, not a fixed wall clock. Two static
544
569
  `options` tune it:
545
570
 
546
- | Option | Default | Behavior |
547
- | -------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
548
- | `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. |
549
- | `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. |
571
+ | Option | Default | Behavior |
572
+ | -------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
573
+ | `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. |
574
+ | `agentToolReattachMaxWindowMs` | `Infinity` | Optional hard wall-clock ceiling on a single re-attach. Uncapped by default, so a healthy, long-running child is never cut off; a runaway child is bounded by its own `chatRecovery` budgets. Set a finite value to impose a cap. |
550
575
 
551
576
  Give-up outcomes map to the `AgentToolFailure` fields:
552
577
 
@@ -105,7 +105,7 @@ async () => {
105
105
  };
106
106
  ```
107
107
 
108
- To discover protocol surface, the model calls `cdp.spec()` — the live, normalized CDP protocol description (domains with commands, events, and types) — or uses the runtime's built-in `codemode.search` / `codemode.describe`.
108
+ To discover protocol surface, the model calls `cdp.spec()` — the live, normalized CDP protocol description (domains with commands, events, and types, including each command's parameters and return values) — or uses the runtime's built-in `codemode.search` / `codemode.describe`.
109
109
 
110
110
  ## Use with an Agent
111
111
 
@@ -205,6 +205,57 @@ await connector.sweep(); // reclaim expired/stale sessions — call from a sched
205
205
  await runtime.expirePaused(); // reject stale never-approved pauses, freeing their sessions
206
206
  ```
207
207
 
208
+ ## Persistent browser
209
+
210
+ `browserTool` gives the model one browser that stays open between turns, so tabs, cookies, and logins carry over. You create the browser once on your agent and pass it in; the model never starts or closes it. This example uses a [Think](../think/index.md) agent, whose `getTools()` runs every turn:
211
+
212
+ ```ts
213
+ import { Think } from "@cloudflare/think";
214
+ import { Browser, browserRun } from "agents/browser";
215
+ import { browserTool } from "agents/browser/ai-sdk";
216
+
217
+ export class MyAgent extends Think<Env> {
218
+ browser = new Browser({ provider: browserRun(this.env.BROWSER) });
219
+
220
+ constructor(ctx: DurableObjectState, env: Env) {
221
+ super(ctx, env);
222
+ this.lifecycle.use(this.browser);
223
+ }
224
+
225
+ getModel() {
226
+ return "@cf/moonshotai/kimi-k2.7-code";
227
+ }
228
+
229
+ getTools() {
230
+ return {
231
+ browser: browserTool({ browser: this.browser, loader: this.env.LOADER })
232
+ };
233
+ }
234
+ }
235
+ ```
236
+
237
+ - The model writes CDP code as with `createBrowserTools`. `sessionId: "active"` points at the tab it's working in, and that tab is remembered across turns.
238
+ - Tabs the page opens itself come back as `newTabs` in the result.
239
+ - If the browser was lost (idle past `keepAliveMs`, closed, or crashed), the code still runs in a new browser and the result includes `restarted: true`.
240
+ - Call `this.browser.liveView()` to give a person a Live View link into the same browser, and `this.browser.close()` to shut it down.
241
+ - Set `keepAliveMs`, `recording`, and `guardrails` on `browserRun(binding, options)`. Use a different `name` for each extra browser on the same agent.
242
+
243
+ Only Chromium on a Browser Run binding is supported. It needs the same `LOADER` binding and `CodemodeRuntime` export as `createBrowserTools`.
244
+
245
+ For TanStack AI, import `browserTool` from `agents/browser/tanstack-ai` instead. It takes the same options plus an optional `name` (default `"browser"`) and returns a `ServerTool`:
246
+
247
+ ```ts
248
+ import { browserTool } from "agents/browser/tanstack-ai";
249
+
250
+ const stream = chat({
251
+ adapter,
252
+ tools: [browserTool({ browser: this.browser, loader: this.env.LOADER })],
253
+ messages
254
+ });
255
+ ```
256
+
257
+ The TanStack AI tool doesn't support screenshots yet. TanStack AI gives the host and the model the same output, so if the model returns a screenshot, the tool replaces it with a note saying it was left out, and the tool's instructions tell the model to read the page with `Runtime.evaluate` instead.
258
+
208
259
  ## Quick Actions (stateless browsing)
209
260
 
210
261
  `browser_execute` drives a full, stateful CDP session — the right tool for interactive, multi-step automation. But a lot of agent browsing is really one-shot: _read this page as Markdown_, _extract these fields_, _list the links_. For those, [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) are simpler, faster, and cheaper. They need only the `browser` binding — no Durable Object, Worker Loader, or sandbox — so they work from any Worker.
@@ -404,12 +455,12 @@ Either `browser` or `cdpUrl` must be provided. When both are set, `cdpUrl` takes
404
455
  For custom integrations, import the building blocks directly:
405
456
 
406
457
  ```ts
407
- import { BrowserConnector, CdpSession, connectUrl } from "agents/browser";
458
+ import { BrowserConnector, CdpConnection, connectUrl } from "agents/browser";
408
459
 
409
460
  // Connect to a custom CDP endpoint
410
- const session = await connectUrl("http://localhost:9222");
411
- const version = await session.send("Browser.getVersion");
412
- session.close();
461
+ const connection = await connectUrl("http://localhost:9222");
462
+ const version = await connection.send("Browser.getVersion");
463
+ connection.close();
413
464
 
414
465
  // Or plug the connector into your own codemode runtime
415
466
  const connector = new BrowserConnector(this.ctx, {
@@ -419,6 +470,37 @@ const connector = new BrowserConnector(this.ctx, {
419
470
  });
420
471
  ```
421
472
 
473
+ To load the normalized CDP protocol specification from Worker code, use `loadCdpSpec()`:
474
+
475
+ ```ts
476
+ import { loadCdpSpec } from "agents/browser";
477
+
478
+ const spec = await loadCdpSpec({
479
+ browser: this.env.BROWSER,
480
+ sessionId
481
+ });
482
+ ```
483
+
484
+ Pass a Browser Run binding in `browser` and the Browser Run session ID returned by `createBrowserSession()` in `sessionId`. If you omit `sessionId`, the helper creates a temporary session to fetch the specification, then deletes it. Pass an existing session ID to read the specification without creating another session. If you provide `cdpUrl`, the helper loads the specification from that URL instead; `cdpUrl` takes precedence over `browser` and `sessionId`.
485
+
486
+ You can also manage Browser Rendering sessions directly. `createBrowserSession` accepts [hostname guardrails](https://developers.cloudflare.com/browser-run/features/guardrails/) that restrict which domains the session may reach — fixed at launch for every connection to the session, including Live View (not supported with Kitesurf):
487
+
488
+ ```ts
489
+ import { createBrowserSession, connectBrowserSession } from "agents/browser";
490
+
491
+ const { sessionId } = await createBrowserSession(this.env.BROWSER, {
492
+ guardrails: { allowedDomains: ["example.com", "*.example.com"] }
493
+ });
494
+
495
+ // Connect (and reconnect) without deleting the session on close:
496
+ const session = await connectBrowserSession(this.env.BROWSER, sessionId, {
497
+ timeoutMs: 30_000,
498
+ onClose: () => {
499
+ /* the CDP socket closed; the session itself stays alive */
500
+ }
501
+ });
502
+ ```
503
+
422
504
  ## Local development
423
505
 
424
506
  Recent Wrangler releases support Browser Rendering in local development. `npx wrangler dev` provisions the browser automatically, so the same `browser: env.BROWSER` setup works locally and when deployed.