agents 0.17.4 → 0.18.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 (46) hide show
  1. package/dist/{agent-tool-types-OhWqAbCp.d.ts → agent-tool-types-BNUGGBzQ.d.ts} +30 -11
  2. package/dist/agent-tool-types.d.ts +1 -1
  3. package/dist/{agent-tools-LdNKGZT9.d.ts → agent-tools-BFbzVLFc.d.ts} +2 -2
  4. package/dist/agent-tools.d.ts +1 -1
  5. package/dist/chat/index.d.ts +29 -9
  6. package/dist/chat/index.js +31 -41
  7. package/dist/chat/index.js.map +1 -1
  8. package/dist/chat/react.d.ts +25 -1
  9. package/dist/chat/react.js +249 -93
  10. package/dist/chat/react.js.map +1 -1
  11. package/dist/chat-sdk/index.d.ts +1 -1
  12. package/dist/{client-C7F0MaVz.js → client-CcjiFpTf.js} +176 -42
  13. package/dist/client-CcjiFpTf.js.map +1 -0
  14. package/dist/client.d.ts +1 -1
  15. package/dist/cloudflare-BldFV0Pa.js +117 -0
  16. package/dist/cloudflare-BldFV0Pa.js.map +1 -0
  17. package/dist/index.d.ts +5 -3
  18. package/dist/index.js +118 -43
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/client.d.ts +1 -1
  21. package/dist/mcp/client.js +1 -1
  22. package/dist/mcp/index.d.ts +1 -1
  23. package/dist/mcp/index.js +1 -1
  24. package/dist/observability/ai/index.d.ts +155 -0
  25. package/dist/observability/ai/index.js +1845 -0
  26. package/dist/observability/ai/index.js.map +1 -0
  27. package/dist/react.d.ts +1 -1
  28. package/dist/{retries-CvHJwSuh.d.ts → retries-CAvxtG9d.d.ts} +16 -7
  29. package/dist/retries.d.ts +8 -6
  30. package/dist/retries.js +20 -2
  31. package/dist/retries.js.map +1 -1
  32. package/dist/serializable.d.ts +1 -1
  33. package/dist/sub-routing.d.ts +1 -1
  34. package/dist/vite.d.ts +4 -4
  35. package/dist/vite.js +4 -2
  36. package/dist/vite.js.map +1 -1
  37. package/dist/{wire-types-nflOzNuU.js → wire-types-CU9rLoeS.js} +33 -2
  38. package/dist/wire-types-CU9rLoeS.js.map +1 -0
  39. package/dist/workflows.d.ts +1 -1
  40. package/docs/agent-class.md +9 -1
  41. package/docs/configuration.md +20 -0
  42. package/docs/mcp-client.md +13 -4
  43. package/docs/observability.md +282 -0
  44. package/package.json +7 -2
  45. package/dist/client-C7F0MaVz.js.map +0 -1
  46. package/dist/wire-types-nflOzNuU.js.map +0 -1
@@ -69,11 +69,19 @@ declare class WebSocketChatTransport<
69
69
  private _resumeResolver;
70
70
  private _resumeNoneResolver;
71
71
  private _onStreamPending;
72
+ private _retryResumeProbe;
72
73
  private _expectToolContinuation;
73
74
  private _abortToolContinuation;
74
75
  private _activeServerTurnId;
75
76
  private _cancelAttachedStream;
77
+ private _detachResumeStream;
76
78
  constructor(options: WebSocketChatTransportOptions<ChatMessage>);
79
+ /**
80
+ * Point the singleton transport at a new Agent connection. A pending resolver
81
+ * belongs to the old Chat/socket generation and must settle before messages
82
+ * from the replacement connection can be consumed (#1914 review).
83
+ */
84
+ setAgent(agent: AgentConnection): void;
77
85
  setCancelOnClientAbort(cancelOnClientAbort: boolean): void;
78
86
  /**
79
87
  * Explicitly cancel the active server turn, if any.
@@ -98,6 +106,22 @@ declare class WebSocketChatTransport<
98
106
  * True when the transport is waiting for a resume handshake.
99
107
  */
100
108
  isAwaitingResume(): boolean;
109
+ /**
110
+ * Settle and detach the current handshake without interpreting it as a
111
+ * server-idle response. Used when the owning hook/agent generation changes.
112
+ */
113
+ cancelPendingResume(): boolean;
114
+ /**
115
+ * Invalidate all client-side resume state for an obsolete hook/agent
116
+ * generation without cancelling its durable server turn.
117
+ */
118
+ resetResumeState(): void;
119
+ /**
120
+ * Re-send the active handshake request on the latest socket generation. This
121
+ * preserves one AI SDK resume operation while recovering a request/reply lost
122
+ * with the previous WebSocket.
123
+ */
124
+ retryPendingResume(): boolean;
101
125
  /**
102
126
  * Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUMING.
103
127
  * If reconnectToStream is waiting, this handles the resume handshake
@@ -110,7 +134,7 @@ declare class WebSocketChatTransport<
110
134
  * If reconnectToStream is waiting, resolves the promise with null
111
135
  * immediately (no 5-second timeout). Returns true if handled.
112
136
  */
113
- handleStreamResumeNone(): boolean;
137
+ handleStreamResumeNone(data?: { probeId?: string }): boolean;
114
138
  /**
115
139
  * Called by onAgentMessage when it receives CF_AGENT_STREAM_PENDING (#1784):
116
140
  * the server accepted a turn but its stream has not started yet. If a resume
@@ -1,4 +1,4 @@
1
- import { n as transition } from "../wire-types-nflOzNuU.js";
1
+ import { i as transition, r as STREAM_RESUME_NONE_REASONS } from "../wire-types-CU9rLoeS.js";
2
2
  import { getToolName, isToolUIPart } from "ai";
3
3
  import { nanoid } from "nanoid";
4
4
  import { use, useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -29,15 +29,27 @@ var WebSocketChatTransport = class {
29
29
  this._resumeResolver = null;
30
30
  this._resumeNoneResolver = null;
31
31
  this._onStreamPending = null;
32
+ this._retryResumeProbe = null;
32
33
  this._expectToolContinuation = false;
33
34
  this._abortToolContinuation = null;
34
35
  this._activeServerTurnId = null;
35
36
  this._cancelAttachedStream = null;
37
+ this._detachResumeStream = null;
36
38
  this.agent = options.agent;
37
39
  this.prepareBody = options.prepareBody;
38
40
  this.activeRequestIds = options.activeRequestIds;
39
41
  this.cancelOnClientAbort = options.cancelOnClientAbort ?? false;
40
42
  }
43
+ /**
44
+ * Point the singleton transport at a new Agent connection. A pending resolver
45
+ * belongs to the old Chat/socket generation and must settle before messages
46
+ * from the replacement connection can be consumed (#1914 review).
47
+ */
48
+ setAgent(agent) {
49
+ if (this.agent === agent) return;
50
+ this.resetResumeState();
51
+ this.agent = agent;
52
+ }
41
53
  setCancelOnClientAbort(cancelOnClientAbort) {
42
54
  this.cancelOnClientAbort = cancelOnClientAbort;
43
55
  }
@@ -97,6 +109,35 @@ var WebSocketChatTransport = class {
97
109
  return this._resumeResolver !== null || this._resumeNoneResolver !== null;
98
110
  }
99
111
  /**
112
+ * Settle and detach the current handshake without interpreting it as a
113
+ * server-idle response. Used when the owning hook/agent generation changes.
114
+ */
115
+ cancelPendingResume() {
116
+ const resolveNone = this._resumeNoneResolver;
117
+ if (!resolveNone) return false;
118
+ return resolveNone({});
119
+ }
120
+ /**
121
+ * Invalidate all client-side resume state for an obsolete hook/agent
122
+ * generation without cancelling its durable server turn.
123
+ */
124
+ resetResumeState() {
125
+ this._expectToolContinuation = false;
126
+ this.cancelPendingResume();
127
+ this._detachResumeStream?.();
128
+ }
129
+ /**
130
+ * Re-send the active handshake request on the latest socket generation. This
131
+ * preserves one AI SDK resume operation while recovering a request/reply lost
132
+ * with the previous WebSocket.
133
+ */
134
+ retryPendingResume() {
135
+ const retry = this._retryResumeProbe;
136
+ if (!retry) return false;
137
+ retry();
138
+ return true;
139
+ }
140
+ /**
100
141
  * Called by onAgentMessage when it receives CF_AGENT_STREAM_RESUMING.
101
142
  * If reconnectToStream is waiting, this handles the resume handshake
102
143
  * (ACK + stream creation) and returns true. Otherwise returns false
@@ -112,10 +153,9 @@ var WebSocketChatTransport = class {
112
153
  * If reconnectToStream is waiting, resolves the promise with null
113
154
  * immediately (no 5-second timeout). Returns true if handled.
114
155
  */
115
- handleStreamResumeNone() {
156
+ handleStreamResumeNone(data = {}) {
116
157
  if (!this._resumeNoneResolver) return false;
117
- this._resumeNoneResolver();
118
- return true;
158
+ return this._resumeNoneResolver(data);
119
159
  }
120
160
  /**
121
161
  * Called by onAgentMessage when it receives CF_AGENT_STREAM_PENDING (#1784):
@@ -239,30 +279,47 @@ var WebSocketChatTransport = class {
239
279
  return stream;
240
280
  }
241
281
  async reconnectToStream(_options) {
282
+ if (this.isAwaitingResume()) return null;
242
283
  if (this._expectToolContinuation) {
243
284
  this._expectToolContinuation = false;
244
285
  return this._createToolContinuationStream();
245
286
  }
246
287
  const activeIds = this.activeRequestIds;
288
+ const probeId = nanoid(8);
247
289
  return new Promise((resolve) => {
248
290
  let resolved = false;
249
291
  let timeout;
292
+ let resumeResolver = null;
293
+ let resumeNoneResolver = null;
294
+ let onStreamPending = null;
295
+ let retryResumeProbe = null;
296
+ const clearOwnedCallbacks = () => {
297
+ if (resumeResolver && this._resumeResolver === resumeResolver) this._resumeResolver = null;
298
+ if (resumeNoneResolver && this._resumeNoneResolver === resumeNoneResolver) this._resumeNoneResolver = null;
299
+ if (onStreamPending && this._onStreamPending === onStreamPending) this._onStreamPending = null;
300
+ if (retryResumeProbe && this._retryResumeProbe === retryResumeProbe) this._retryResumeProbe = null;
301
+ };
250
302
  const done = (value) => {
251
303
  if (resolved) return;
252
304
  resolved = true;
253
- this._resumeResolver = null;
254
- this._resumeNoneResolver = null;
255
- this._onStreamPending = null;
305
+ clearOwnedCallbacks();
256
306
  if (timeout) clearTimeout(timeout);
257
307
  resolve(value);
258
308
  };
259
- this._onStreamPending = () => {
260
- if (resolved) return;
309
+ const armTimeout = (delay) => {
261
310
  if (timeout) clearTimeout(timeout);
262
- timeout = setTimeout(() => done(null), RESUME_PENDING_TIMEOUT_MS);
311
+ timeout = setTimeout(() => done(null), delay);
312
+ };
313
+ onStreamPending = () => {
314
+ if (resolved) return;
315
+ armTimeout(RESUME_PENDING_TIMEOUT_MS);
263
316
  };
264
- this._resumeNoneResolver = () => done(null);
265
- this._resumeResolver = (data) => {
317
+ resumeNoneResolver = (data) => {
318
+ if (data.probeId && data.probeId !== probeId) return false;
319
+ done(null);
320
+ return true;
321
+ };
322
+ resumeResolver = (data) => {
266
323
  const requestId = data.id;
267
324
  activeIds?.add(requestId);
268
325
  const stream = this._createResumeStream(requestId);
@@ -272,10 +329,21 @@ var WebSocketChatTransport = class {
272
329
  }));
273
330
  done(stream);
274
331
  };
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);
332
+ retryResumeProbe = () => {
333
+ if (resolved) return;
334
+ armTimeout(RESUME_PROBE_TIMEOUT_MS);
335
+ try {
336
+ this.agent.send(JSON.stringify({
337
+ type: "cf_agent_stream_resume_request",
338
+ probeId
339
+ }));
340
+ } catch {}
341
+ };
342
+ this._onStreamPending = onStreamPending;
343
+ this._resumeNoneResolver = resumeNoneResolver;
344
+ this._resumeResolver = resumeResolver;
345
+ this._retryResumeProbe = retryResumeProbe;
346
+ retryResumeProbe();
279
347
  });
280
348
  }
281
349
  /**
@@ -292,24 +360,26 @@ var WebSocketChatTransport = class {
292
360
  abortError.name = "AbortError";
293
361
  let completed = false;
294
362
  let requestId = null;
295
- let readerController;
363
+ let readerController = null;
364
+ const probeId = nanoid(8);
296
365
  let onResumeRef = null;
297
366
  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;
367
+ let onStreamPendingRef = null;
368
+ let retryResumeProbeRef = null;
369
+ let abortToolContinuationRef = null;
370
+ let detachResumeStreamRef = null;
371
+ const clearOwnedHandshake = () => {
372
+ if (onResumeRef && this._resumeResolver === onResumeRef) this._resumeResolver = null;
373
+ if (onResumeNoneRef && this._resumeNoneResolver === onResumeNoneRef) this._resumeNoneResolver = null;
374
+ if (onStreamPendingRef && this._onStreamPending === onStreamPendingRef) this._onStreamPending = null;
375
+ if (retryResumeProbeRef && this._retryResumeProbe === retryResumeProbeRef) this._retryResumeProbe = null;
306
376
  };
307
- const finish = (action, resumeResolver, resumeNoneResolver, keepRequestId = false) => {
377
+ const finish = (action, keepRequestId = false) => {
308
378
  if (completed) return;
309
379
  completed = true;
310
- this._abortToolContinuation = null;
311
- this._onStreamPending = null;
312
- clearHandshakeResolvers(resumeResolver, resumeNoneResolver);
380
+ if (this._abortToolContinuation === abortToolContinuationRef) this._abortToolContinuation = null;
381
+ if (this._detachResumeStream === detachResumeStreamRef) this._detachResumeStream = null;
382
+ clearOwnedHandshake();
313
383
  try {
314
384
  action();
315
385
  } catch {}
@@ -317,10 +387,10 @@ var WebSocketChatTransport = class {
317
387
  streamController.abort();
318
388
  };
319
389
  const transport = this;
320
- this._abortToolContinuation = () => {
390
+ abortToolContinuationRef = () => {
321
391
  if (completed) return false;
322
392
  if (requestId === null) {
323
- finish(() => readerController.error(abortError), onResumeRef, onResumeNoneRef);
393
+ finish(() => readerController?.error(abortError));
324
394
  return true;
325
395
  }
326
396
  try {
@@ -329,71 +399,87 @@ var WebSocketChatTransport = class {
329
399
  id: requestId
330
400
  }));
331
401
  } catch {}
332
- finish(() => readerController.error(abortError), onResumeRef, onResumeNoneRef, true);
402
+ finish(() => readerController?.error(abortError), true);
333
403
  return true;
334
404
  };
405
+ this._abortToolContinuation = abortToolContinuationRef;
406
+ detachResumeStreamRef = () => {
407
+ if (completed) return false;
408
+ finish(() => readerController?.close());
409
+ return true;
410
+ };
411
+ this._detachResumeStream = detachResumeStreamRef;
335
412
  return new ReadableStream({
336
413
  start(controller) {
337
414
  readerController = controller;
338
415
  let timeout;
339
- const onResumeNone = () => {
416
+ const armTimeout = (delay) => {
340
417
  if (timeout) clearTimeout(timeout);
341
- finish(() => controller.close(), onResume, onResumeNone);
418
+ timeout = setTimeout(() => finish(() => controller.close()), delay);
419
+ };
420
+ const onResumeNone = (data) => {
421
+ if (data.probeId && data.probeId !== probeId) return false;
422
+ finish(() => controller.close());
423
+ return true;
342
424
  };
343
425
  const onResume = (data) => {
344
426
  if (requestId) return;
345
427
  requestId = data.id;
346
428
  activeIds?.add(requestId);
347
- clearHandshakeResolvers(onResume, onResumeNone);
348
- transport._onStreamPending = null;
429
+ clearOwnedHandshake();
349
430
  if (timeout) clearTimeout(timeout);
350
431
  agent.send(JSON.stringify({
351
432
  type: "cf_agent_stream_resume_ack",
352
433
  id: requestId
353
434
  }));
354
435
  };
355
- onResumeRef = onResume;
356
- onResumeNoneRef = onResumeNone;
357
- timeout = setTimeout(() => finish(() => controller.close(), onResume, onResumeNone), RESUME_PROBE_TIMEOUT_MS);
358
- transport._onStreamPending = () => {
436
+ const onStreamPending = () => {
359
437
  if (completed) return;
360
- if (timeout) clearTimeout(timeout);
361
- timeout = setTimeout(() => finish(() => controller.close(), onResume, onResumeNone), RESUME_PENDING_TIMEOUT_MS);
438
+ armTimeout(RESUME_PENDING_TIMEOUT_MS);
439
+ };
440
+ const retryResumeProbe = () => {
441
+ if (completed || requestId !== null) return;
442
+ armTimeout(RESUME_PROBE_TIMEOUT_MS);
443
+ try {
444
+ transport.agent.send(JSON.stringify({
445
+ type: "cf_agent_stream_resume_request",
446
+ probeId
447
+ }));
448
+ } catch {}
362
449
  };
450
+ onResumeRef = onResume;
451
+ onResumeNoneRef = onResumeNone;
452
+ onStreamPendingRef = onStreamPending;
453
+ retryResumeProbeRef = retryResumeProbe;
363
454
  transport._resumeResolver = onResume;
364
455
  transport._resumeNoneResolver = onResumeNone;
456
+ transport._onStreamPending = onStreamPending;
457
+ transport._retryResumeProbe = retryResumeProbe;
365
458
  const onMessage = (event) => {
366
459
  try {
367
460
  const data = JSON.parse(event.data);
368
461
  if (data.type !== "cf_agent_use_chat_response" || requestId == null || data.id !== requestId) return;
369
462
  if (data.error) {
370
- finish(() => controller.error(new Error(data.body || "Stream error")), onResume, onResumeNone);
463
+ finish(() => controller.error(new Error(data.body || "Stream error")));
371
464
  return;
372
465
  }
373
466
  if (data.body?.trim()) try {
374
467
  const chunk = JSON.parse(data.body);
375
468
  controller.enqueue(chunk);
376
469
  } catch {}
377
- if (data.done) finish(() => controller.close(), onResume, onResumeNone);
470
+ if (data.done) finish(() => controller.close());
378
471
  } catch {}
379
472
  };
380
- const onClose = () => {
381
- if (timeout) clearTimeout(timeout);
382
- finish(() => controller.close(), onResume, onResumeNone);
383
- };
473
+ const onClose = () => finish(() => controller.close());
384
474
  agent.addEventListener("message", onMessage, { signal: streamController.signal });
385
475
  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
- }
476
+ retryResumeProbe();
391
477
  },
392
478
  cancel() {
393
479
  if (requestId && transport.cancelOnClientAbort) {
394
480
  transport.sendCancelFrame(requestId);
395
- finish(() => {}, onResumeRef, onResumeNoneRef, true);
396
- } else finish(() => {}, onResumeRef, onResumeNoneRef);
481
+ finish(() => {}, true);
482
+ } else finish(() => {});
397
483
  }
398
484
  });
399
485
  }
@@ -408,24 +494,32 @@ var WebSocketChatTransport = class {
408
494
  const abortError = /* @__PURE__ */ new Error("Aborted");
409
495
  abortError.name = "AbortError";
410
496
  let completed = false;
497
+ let detachResumeStream = null;
411
498
  const finish = (action, keepId = false, clearServerTurn = true) => {
412
499
  if (completed) return;
413
500
  completed = true;
414
501
  if (clearServerTurn) this.clearActiveServerTurn(requestId);
502
+ if (this._detachResumeStream === detachResumeStream) this._detachResumeStream = null;
415
503
  try {
416
504
  action();
417
505
  } catch {}
418
506
  if (!keepId) activeIds?.delete(requestId);
419
507
  chunkController.abort();
420
508
  };
509
+ let streamController = null;
421
510
  const cancelActiveRequest = () => {
422
511
  if (completed) return false;
423
- finish(() => streamController.error(abortError), true);
512
+ finish(() => streamController?.error(abortError), true);
424
513
  return true;
425
514
  };
426
515
  this.setActiveServerTurn(requestId, cancelActiveRequest);
427
- let streamController;
428
516
  const transport = this;
517
+ detachResumeStream = () => {
518
+ if (completed) return false;
519
+ finish(() => streamController?.close());
520
+ return true;
521
+ };
522
+ this._detachResumeStream = detachResumeStream;
429
523
  return new ReadableStream({
430
524
  start(controller) {
431
525
  streamController = controller;
@@ -763,7 +857,7 @@ function useAgentChat(options) {
763
857
  return extraBody;
764
858
  }
765
859
  });
766
- customTransportRef.current.agent = agentRef.current;
860
+ customTransportRef.current.setAgent(agentRef.current);
767
861
  customTransportRef.current.setCancelOnClientAbort(cancelOnClientAbort);
768
862
  const customTransport = customTransportRef.current;
769
863
  const useChatHelpers = useChat({
@@ -772,16 +866,36 @@ function useAgentChat(options) {
772
866
  messages: initialMessages,
773
867
  transport: customTransport,
774
868
  id: stableChatIdRef.current,
775
- resume
869
+ resume: false
776
870
  });
777
- const { messages: chatMessages, setMessages, addToolResult, addToolApprovalResponse, sendMessage, resumeStream, status, stop } = useChatHelpers;
871
+ const { messages: chatMessages, setMessages, addToolResult, addToolApprovalResponse, sendMessage, resumeStream: rawResumeStream, status, stop } = useChatHelpers;
778
872
  const statusRef = useRef(status);
779
873
  statusRef.current = status;
874
+ const resumeGenerationRef = useRef(0);
875
+ const resumeOperationRef = useRef(null);
876
+ const reconnectProbePendingRef = useRef(false);
877
+ const reconnectProbeRunnerRef = useRef(null);
878
+ const invalidateResumeGeneration = useCallback(() => {
879
+ resumeGenerationRef.current++;
880
+ resumeOperationRef.current = null;
881
+ }, []);
882
+ const resumeStream = useCallback((...args) => {
883
+ const active = resumeOperationRef.current;
884
+ if (active) return active.promise;
885
+ const operation = {
886
+ generation: resumeGenerationRef.current,
887
+ promise: Promise.resolve()
888
+ };
889
+ resumeOperationRef.current = operation;
890
+ operation.promise = rawResumeStream(...args).finally(() => {
891
+ if (resumeOperationRef.current !== operation || resumeGenerationRef.current !== operation.generation) return;
892
+ resumeOperationRef.current = null;
893
+ reconnectProbeRunnerRef.current?.();
894
+ });
895
+ return operation.promise;
896
+ }, [rawResumeStream]);
780
897
  const resumeStreamRef = useRef(resumeStream);
781
898
  resumeStreamRef.current = resumeStream;
782
- const hasConnectedOnceRef = useRef(false);
783
- const resumeInFlightRef = useRef(false);
784
- const resumeGenerationRef = useRef(0);
785
899
  const resumingToolContinuationRef = useRef(false);
786
900
  const pendingToolContinuationRef = useRef(false);
787
901
  const observedToolContinuationRequestIdRef = useRef(null);
@@ -801,20 +915,24 @@ function useAgentChat(options) {
801
915
  }, []);
802
916
  const scheduleToolContinuationLaunch = useCallback(() => {
803
917
  if (!pendingToolContinuationRef.current || statusRef.current !== "ready" || continuationLaunchTimerRef.current) return;
804
- continuationLaunchTimerRef.current = setTimeout(() => {
805
- continuationLaunchTimerRef.current = null;
806
- if (!pendingToolContinuationRef.current || statusRef.current !== "ready") return;
807
- pendingToolContinuationRef.current = false;
808
- const myGeneration = continuationGenerationRef.current;
809
- customTransport.expectToolContinuation();
810
- resumeStream().catch((error) => {
811
- console.error("[useAgentChat] Tool continuation resume failed:", error);
812
- }).finally(() => {
813
- if (continuationGenerationRef.current !== myGeneration) return;
814
- resumingToolContinuationRef.current = false;
815
- setIsToolContinuation(false);
816
- });
918
+ const timer = setTimeout(() => {
919
+ (async () => {
920
+ while (resumeOperationRef.current) await resumeOperationRef.current.promise.catch(() => {});
921
+ if (continuationLaunchTimerRef.current === timer) continuationLaunchTimerRef.current = null;
922
+ if (!pendingToolContinuationRef.current || statusRef.current !== "ready") return;
923
+ pendingToolContinuationRef.current = false;
924
+ const myGeneration = continuationGenerationRef.current;
925
+ customTransport.expectToolContinuation();
926
+ await resumeStream().catch((error) => {
927
+ console.error("[useAgentChat] Tool continuation resume failed:", error);
928
+ }).finally(() => {
929
+ if (continuationGenerationRef.current !== myGeneration) return;
930
+ resumingToolContinuationRef.current = false;
931
+ setIsToolContinuation(false);
932
+ });
933
+ })();
817
934
  }, 0);
935
+ continuationLaunchTimerRef.current = timer;
818
936
  }, [customTransport, resumeStream]);
819
937
  const startToolContinuation = useCallback(() => {
820
938
  if (!autoContinueAfterToolResult || resumingToolContinuationRef.current) return;
@@ -1203,7 +1321,12 @@ function useAgentChat(options) {
1203
1321
  });
1204
1322
  break;
1205
1323
  case "cf_agent_stream_resume_none":
1206
- customTransport.handleStreamResumeNone();
1324
+ if (customTransport.handleStreamResumeNone(data) && data.reason === STREAM_RESUME_NONE_REASONS.IDLE && typeof data.probeId === "string") {
1325
+ const result = transition(streamStateRef.current, { type: "clear" });
1326
+ streamStateRef.current = result.state;
1327
+ setIsServerStreaming(result.isStreaming);
1328
+ if (observedToolContinuationRequestIdRef.current !== null) resetToolContinuation();
1329
+ }
1207
1330
  break;
1208
1331
  case "cf_agent_stream_pending":
1209
1332
  customTransport.handleStreamPending();
@@ -1274,6 +1397,7 @@ function useAgentChat(options) {
1274
1397
  localResponseIds.delete(data.id);
1275
1398
  localRequestIdsRef.current.delete(data.id);
1276
1399
  fallbackAckedResumeRequestIdsRef.current.delete(data.id);
1400
+ if (observedToolContinuationRequestIdRef.current === data.id) resetToolContinuation();
1277
1401
  }
1278
1402
  return;
1279
1403
  }
@@ -1317,28 +1441,51 @@ function useAgentChat(options) {
1317
1441
  }
1318
1442
  }
1319
1443
  const fallbackAckedResumeRequestIds = fallbackAckedResumeRequestIdsRef.current;
1444
+ let socketIsOpen = false;
1445
+ let sawClose = false;
1446
+ let disposed = false;
1447
+ const clearFallbackObserver = () => {
1448
+ const result = transition(streamStateRef.current, { type: "clear" });
1449
+ streamStateRef.current = result.state;
1450
+ setIsServerStreaming(result.isStreaming);
1451
+ if (observedToolContinuationRequestIdRef.current !== null) resetToolContinuation();
1452
+ };
1453
+ const tryPendingReconnectProbe = () => {
1454
+ if (disposed || !socketIsOpen || !reconnectProbePendingRef.current) return;
1455
+ if (!resume) {
1456
+ reconnectProbePendingRef.current = false;
1457
+ return;
1458
+ }
1459
+ if (customTransport.retryPendingResume()) {
1460
+ reconnectProbePendingRef.current = false;
1461
+ return;
1462
+ }
1463
+ const canReconcileObservedContinuation = observedToolContinuationRequestIdRef.current !== null;
1464
+ if (statusRef.current !== "ready" && statusRef.current !== "error" || resumingToolContinuationRef.current && !canReconcileObservedContinuation || resumeOperationRef.current !== null) return;
1465
+ reconnectProbePendingRef.current = false;
1466
+ resumeStreamRef.current().catch(() => {});
1467
+ };
1468
+ reconnectProbeRunnerRef.current = tryPendingReconnectProbe;
1320
1469
  function onAgentClose() {
1470
+ socketIsOpen = false;
1471
+ sawClose = true;
1321
1472
  fallbackAckedResumeRequestIds.clear();
1473
+ if (!resume) clearFallbackObserver();
1322
1474
  }
1323
1475
  function onAgentOpen() {
1324
- if (!hasConnectedOnceRef.current) {
1325
- hasConnectedOnceRef.current = true;
1326
- return;
1327
- }
1328
- if (!resume || statusRef.current !== "ready" || resumingToolContinuationRef.current || resumeInFlightRef.current || customTransport.isAwaitingResume()) return;
1329
- const resumeStreamFn = resumeStreamRef.current;
1330
- if (!resumeStreamFn) return;
1331
- resumeInFlightRef.current = true;
1332
- const myGeneration = resumeGenerationRef.current;
1333
- Promise.resolve(resumeStreamFn()).catch(() => {}).finally(() => {
1334
- if (resumeGenerationRef.current !== myGeneration) return;
1335
- resumeInFlightRef.current = false;
1336
- });
1476
+ socketIsOpen = true;
1477
+ if (!sawClose) return;
1478
+ sawClose = false;
1479
+ reconnectProbePendingRef.current = true;
1480
+ tryPendingReconnectProbe();
1337
1481
  }
1338
1482
  agent.addEventListener("message", onAgentMessage);
1339
1483
  agent.addEventListener("close", onAgentClose);
1340
1484
  agent.addEventListener("open", onAgentOpen);
1341
1485
  return () => {
1486
+ disposed = true;
1487
+ if (reconnectProbeRunnerRef.current === tryPendingReconnectProbe) reconnectProbeRunnerRef.current = null;
1488
+ reconnectProbePendingRef.current = false;
1342
1489
  agent.removeEventListener("message", onAgentMessage);
1343
1490
  agent.removeEventListener("close", onAgentClose);
1344
1491
  agent.removeEventListener("open", onAgentOpen);
@@ -1348,8 +1495,8 @@ function useAgentChat(options) {
1348
1495
  setIsRecovering(false);
1349
1496
  protectedStreamingAssistantRef.current = null;
1350
1497
  localResponseIds.clear();
1351
- resumeGenerationRef.current++;
1352
- resumeInFlightRef.current = false;
1498
+ customTransport.resetResumeState();
1499
+ invalidateResumeGeneration();
1353
1500
  };
1354
1501
  }, [
1355
1502
  agent,
@@ -1360,8 +1507,16 @@ function useAgentChat(options) {
1360
1507
  resetToolContinuation,
1361
1508
  resetMatchingHydratedAssistantForReplay,
1362
1509
  restoreProtectedStreamingAssistant,
1363
- resetLocalChatState
1510
+ resetLocalChatState,
1511
+ invalidateResumeGeneration
1364
1512
  ]);
1513
+ useEffect(() => {
1514
+ if (!resume) return;
1515
+ resumeStream().catch(() => {});
1516
+ }, [resume, resumeStream]);
1517
+ useEffect(() => {
1518
+ reconnectProbeRunnerRef.current?.();
1519
+ }, [isToolContinuation, status]);
1365
1520
  const addToolResultAndSendMessage = async (args) => {
1366
1521
  const { toolCallId } = args;
1367
1522
  const toolName = "tool" in args ? args.tool : "";
@@ -1464,6 +1619,7 @@ function useAgentChat(options) {
1464
1619
  const isStreaming = status === "streaming" || effectiveIsServerStreaming;
1465
1620
  return {
1466
1621
  ...useChatHelpers,
1622
+ resumeStream,
1467
1623
  messages: messagesWithToolResults,
1468
1624
  isServerStreaming: effectiveIsServerStreaming,
1469
1625
  isStreaming,