agents-vue 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,498 +1,14 @@
1
1
  import { Chat } from '@ai-sdk/vue';
2
2
  import { broadcastTransition } from 'agents/chat';
3
+ import { WebSocketChatTransport } from 'agents/chat/transport';
3
4
  import { AgentClient } from 'agents/client';
4
- import { ref, onScopeDispose } from 'vue';
5
-
6
- function randomRequestId() {
7
- return Math.random().toString(36).slice(2, 10);
8
- }
9
- const RESUME_PROBE_TIMEOUT_MS = 5e3;
10
- const RESUME_PENDING_TIMEOUT_MS = 6e4;
11
- class WebSocketChatTransport {
12
- socket;
13
- prepareBody;
14
- activeRequestIds;
15
- // Resume handshake state (public so the composable message handler can resolve them)
16
- _resumeResolver = null;
17
- _resumeNoneResolver = null;
18
- // Tool continuation state
19
- _expectToolContinuation = false;
20
- _abortToolContinuation = null;
21
- // Whether a generic client abort should cancel the server turn (default: local-only).
22
- cancelOnClientAbort;
23
- // The currently cancellable server turn — either locally initiated (sendMessages)
24
- // or observed via the broadcast/resume-fallback path. `_cancelAttachedStream` tears
25
- // down the local stream attached to a locally-initiated turn (null for observed turns).
26
- _activeServerTurnId = null;
27
- _cancelAttachedStream = null;
28
- // Pre-stream "pending" extender for the in-flight resume probe (cleared on resolve).
29
- _onStreamPending = null;
30
- constructor(options) {
31
- this.socket = options.socket;
32
- this.prepareBody = options.prepareBody;
33
- this.activeRequestIds = options.activeRequestIds ?? /* @__PURE__ */ new Set();
34
- this.cancelOnClientAbort = options.cancelOnClientAbort ?? false;
35
- }
36
- /** Mark that the next reconnectToStream() should attach to a server-initiated tool continuation. */
37
- expectToolContinuation() {
38
- this._expectToolContinuation = true;
39
- }
40
- /** Abort the active client-side tool continuation stream. */
41
- abortActiveToolContinuation() {
42
- return this._abortToolContinuation?.() ?? false;
43
- }
44
- /** Send a server-turn cancel frame for a request id (best-effort). */
45
- sendCancelFrame(id) {
46
- try {
47
- this.socket.send(JSON.stringify({ id, type: "cf_agent_chat_request_cancel" }));
48
- } catch {
49
- }
50
- }
51
- setActiveServerTurn(id, cancelStream) {
52
- this._activeServerTurnId = id;
53
- this._cancelAttachedStream = cancelStream;
54
- }
55
- clearActiveServerTurn(id) {
56
- if (this._activeServerTurnId === id) {
57
- this._activeServerTurnId = null;
58
- this._cancelAttachedStream = null;
59
- }
60
- }
61
- /** Register a server-observed (broadcast / resume-fallback) turn as the cancellable one. */
62
- observeServerTurn(requestId) {
63
- this.setActiveServerTurn(requestId, null);
64
- }
65
- /** Mark a server turn complete so it is no longer the cancellable turn. */
66
- handleServerTurnCompleted(requestId) {
67
- this.clearActiveServerTurn(requestId);
68
- }
69
- /**
70
- * Explicitly cancel the active server turn (and any tool continuation). Unlike a
71
- * generic client abort, this always signals the server. Drives `useAgentChat.stop()`.
72
- */
73
- cancelActiveServerTurn() {
74
- const id = this._activeServerTurnId;
75
- let cancelledRequest = false;
76
- if (id) {
77
- this.sendCancelFrame(id);
78
- this._cancelAttachedStream?.();
79
- this.clearActiveServerTurn(id);
80
- cancelledRequest = true;
81
- }
82
- const cancelledToolContinuation = this.abortActiveToolContinuation();
83
- return cancelledRequest || cancelledToolContinuation;
84
- }
85
- /** Called by the composable on cf_agent_stream_pending — extend the in-flight resume probe. */
86
- handleStreamPending() {
87
- if (!this._onStreamPending) {
88
- return false;
89
- }
90
- this._onStreamPending();
91
- return true;
92
- }
93
- /** True when the transport is waiting for a resume handshake. */
94
- isAwaitingResume() {
95
- return this._resumeResolver !== null || this._resumeNoneResolver !== null;
96
- }
97
- /** Called by the composable when it receives cf_agent_stream_resuming. */
98
- handleStreamResuming(data) {
99
- if (!this._resumeResolver) {
100
- return false;
101
- }
102
- this._resumeResolver(data);
103
- return true;
104
- }
105
- /** Called by the composable when it receives cf_agent_stream_resume_none. */
106
- handleStreamResumeNone() {
107
- if (!this._resumeNoneResolver) {
108
- return false;
109
- }
110
- this._resumeNoneResolver();
111
- return true;
112
- }
113
- async sendMessages(options) {
114
- const requestId = randomRequestId();
115
- const abortController = new AbortController();
116
- let completed = false;
117
- let extraBody = {};
118
- if (this.prepareBody) {
119
- extraBody = await this.prepareBody({
120
- messages: options.messages,
121
- trigger: options.trigger,
122
- messageId: options.messageId
123
- });
124
- }
125
- if (options.body) {
126
- extraBody = { ...extraBody, ...options.body };
127
- }
128
- const tail = options.messages.at(-1);
129
- const bodyPayload = JSON.stringify({
130
- messages: tail ? [tail] : [],
131
- trigger: options.trigger,
132
- ...extraBody
133
- });
134
- this.activeRequestIds.add(requestId);
135
- const socket = this.socket;
136
- const activeIds = this.activeRequestIds;
137
- const finish = (action, keepId = false, clearServerTurn = true) => {
138
- if (completed) {
139
- return;
140
- }
141
- completed = true;
142
- if (clearServerTurn) {
143
- this.clearActiveServerTurn(requestId);
144
- }
145
- try {
146
- action();
147
- } catch {
148
- }
149
- if (!keepId) {
150
- activeIds.delete(requestId);
151
- }
152
- abortController.abort();
153
- };
154
- const abortError = new Error("Aborted");
155
- abortError.name = "AbortError";
156
- let requestSent = false;
157
- let streamController;
158
- const cancelActiveRequest = () => {
159
- if (completed) {
160
- return false;
161
- }
162
- finish(() => streamController.error(abortError), true);
163
- return true;
164
- };
165
- const onAbort = () => {
166
- if (completed) {
167
- return;
168
- }
169
- if (this.cancelOnClientAbort) {
170
- if (requestSent) {
171
- this.sendCancelFrame(requestId);
172
- }
173
- finish(() => streamController.error(abortError), requestSent);
174
- } else {
175
- finish(() => streamController.error(abortError), false, !requestSent);
176
- }
177
- };
178
- const stream = new ReadableStream({
179
- start(controller) {
180
- streamController = controller;
181
- const onMessage = (event) => {
182
- try {
183
- const data = JSON.parse(String(event.data));
184
- if (data.type !== "cf_agent_use_chat_response") {
185
- return;
186
- }
187
- if (data.id !== requestId) {
188
- return;
189
- }
190
- if (data.error) {
191
- finish(() => controller.error(new Error(data.body || "Stream error")));
192
- return;
193
- }
194
- if (data.body?.trim()) {
195
- try {
196
- const chunk = JSON.parse(data.body);
197
- controller.enqueue(chunk);
198
- } catch {
199
- }
200
- }
201
- if (data.done) {
202
- finish(() => controller.close());
203
- }
204
- } catch {
205
- }
206
- };
207
- socket.addEventListener("message", onMessage, { signal: abortController.signal });
208
- },
209
- cancel() {
210
- onAbort();
211
- }
212
- });
213
- this.setActiveServerTurn(requestId, cancelActiveRequest);
214
- if (options.abortSignal) {
215
- options.abortSignal.addEventListener("abort", onAbort, { once: true });
216
- if (options.abortSignal.aborted) {
217
- onAbort();
218
- }
219
- }
220
- if (!completed) {
221
- socket.send(
222
- JSON.stringify({
223
- id: requestId,
224
- init: { method: "POST", body: bodyPayload },
225
- type: "cf_agent_use_chat_request"
226
- })
227
- );
228
- requestSent = true;
229
- }
230
- return stream;
231
- }
232
- async reconnectToStream(_options) {
233
- if (this._expectToolContinuation) {
234
- this._expectToolContinuation = false;
235
- return this._createToolContinuationStream();
236
- }
237
- const activeIds = this.activeRequestIds;
238
- return new Promise((resolve) => {
239
- let resolved = false;
240
- let timeout;
241
- const done = (value) => {
242
- if (resolved) {
243
- return;
244
- }
245
- resolved = true;
246
- this._resumeResolver = null;
247
- this._resumeNoneResolver = null;
248
- this._onStreamPending = null;
249
- clearTimeout(timeout);
250
- resolve(value);
251
- };
252
- this._resumeNoneResolver = () => done(null);
253
- this._resumeResolver = (data) => {
254
- const id = data.id;
255
- activeIds.add(id);
256
- this.socket.send(JSON.stringify({ type: "cf_agent_stream_resume_ack", id }));
257
- done(this._createResumeStream(id));
258
- };
259
- this._onStreamPending = () => {
260
- if (resolved) {
261
- return;
262
- }
263
- clearTimeout(timeout);
264
- timeout = setTimeout(() => done(null), RESUME_PENDING_TIMEOUT_MS);
265
- };
266
- try {
267
- this.socket.send(JSON.stringify({ type: "cf_agent_stream_resume_request" }));
268
- } catch {
269
- }
270
- timeout = setTimeout(() => done(null), RESUME_PROBE_TIMEOUT_MS);
271
- });
272
- }
273
- /**
274
- * Creates a deferred ReadableStream for client-side tool continuations.
275
- * Returned immediately so AI SDK status becomes "submitted" right after tool output,
276
- * then waits for the server to announce continuation via STREAM_RESUMING.
277
- */
278
- _createToolContinuationStream() {
279
- const socket = this.socket;
280
- const activeIds = this.activeRequestIds;
281
- const streamAbort = new AbortController();
282
- const abortError = new Error("Aborted");
283
- abortError.name = "AbortError";
284
- let completed = false;
285
- let requestId = null;
286
- let readerController;
287
- let onResumeRef = null;
288
- let onResumeNoneRef = null;
289
- const clearHandshakeResolvers = (resumeResolver, resumeNoneResolver) => {
290
- if (resumeResolver === void 0 && resumeNoneResolver === void 0) {
291
- this._resumeResolver = null;
292
- this._resumeNoneResolver = null;
293
- return;
294
- }
295
- if (resumeResolver && this._resumeResolver === resumeResolver) {
296
- this._resumeResolver = null;
297
- }
298
- if (resumeNoneResolver && this._resumeNoneResolver === resumeNoneResolver) {
299
- this._resumeNoneResolver = null;
300
- }
301
- };
302
- const finish = (action, resumeResolver, resumeNoneResolver, keepRequestId = false) => {
303
- if (completed) {
304
- return;
305
- }
306
- completed = true;
307
- this._abortToolContinuation = null;
308
- this._onStreamPending = null;
309
- clearHandshakeResolvers(resumeResolver, resumeNoneResolver);
310
- try {
311
- action();
312
- } catch {
313
- }
314
- if (requestId && !keepRequestId) {
315
- activeIds.delete(requestId);
316
- }
317
- streamAbort.abort();
318
- };
319
- this._abortToolContinuation = () => {
320
- if (completed) {
321
- return false;
322
- }
323
- if (requestId === null) {
324
- finish(() => readerController.error(abortError), onResumeRef, onResumeNoneRef);
325
- return true;
326
- }
327
- try {
328
- socket.send(JSON.stringify({ type: "cf_agent_chat_request_cancel", id: requestId }));
329
- } catch {
330
- }
331
- finish(() => readerController.error(abortError), onResumeRef, onResumeNoneRef, true);
332
- return true;
333
- };
334
- return new ReadableStream({
335
- start: (controller) => {
336
- readerController = controller;
337
- const onResumeNone = () => {
338
- clearTimeout(timeout);
339
- finish(() => controller.close(), onResume, onResumeNone);
340
- };
341
- const onResume = (data) => {
342
- if (requestId) {
343
- return;
344
- }
345
- requestId = data.id;
346
- activeIds.add(requestId);
347
- clearHandshakeResolvers(onResume, onResumeNone);
348
- this._onStreamPending = null;
349
- clearTimeout(timeout);
350
- socket.send(JSON.stringify({ type: "cf_agent_stream_resume_ack", id: requestId }));
351
- };
352
- onResumeRef = onResume;
353
- onResumeNoneRef = onResumeNone;
354
- let timeout = setTimeout(
355
- () => finish(() => controller.close(), onResume, onResumeNone),
356
- RESUME_PROBE_TIMEOUT_MS
357
- );
358
- this._onStreamPending = () => {
359
- if (completed) {
360
- return;
361
- }
362
- clearTimeout(timeout);
363
- timeout = setTimeout(
364
- () => finish(() => controller.close(), onResume, onResumeNone),
365
- RESUME_PENDING_TIMEOUT_MS
366
- );
367
- };
368
- this._resumeResolver = onResume;
369
- this._resumeNoneResolver = onResumeNone;
370
- const onMessage = (event) => {
371
- try {
372
- const data = JSON.parse(String(event.data));
373
- if (data.type !== "cf_agent_use_chat_response" || requestId == null || data.id !== requestId) {
374
- return;
375
- }
376
- if (data.error) {
377
- finish(() => controller.error(new Error(data.body || "Stream error")), onResume, onResumeNone);
378
- return;
379
- }
380
- if (data.body?.trim()) {
381
- try {
382
- const chunk = JSON.parse(data.body);
383
- controller.enqueue(chunk);
384
- } catch {
385
- }
386
- }
387
- if (data.done) {
388
- finish(() => controller.close(), onResume, onResumeNone);
389
- }
390
- } catch {
391
- }
392
- };
393
- socket.addEventListener("message", onMessage, { signal: streamAbort.signal });
394
- try {
395
- socket.send(JSON.stringify({ type: "cf_agent_stream_resume_request" }));
396
- } catch {
397
- finish(() => controller.close());
398
- }
399
- },
400
- cancel: () => {
401
- if (requestId && this.cancelOnClientAbort) {
402
- this.sendCancelFrame(requestId);
403
- finish(() => {
404
- }, onResumeRef, onResumeNoneRef, true);
405
- } else {
406
- finish(() => {
407
- }, onResumeRef, onResumeNoneRef);
408
- }
409
- }
410
- });
411
- }
412
- /** Creates a ReadableStream that receives resumed stream chunks. */
413
- _createResumeStream(requestId) {
414
- const socket = this.socket;
415
- const activeIds = this.activeRequestIds;
416
- const chunkAbort = new AbortController();
417
- const abortError = new Error("Aborted");
418
- abortError.name = "AbortError";
419
- let completed = false;
420
- let streamController;
421
- const finish = (action, keepId = false, clearServerTurn = true) => {
422
- if (completed) {
423
- return;
424
- }
425
- completed = true;
426
- if (clearServerTurn) {
427
- this.clearActiveServerTurn(requestId);
428
- }
429
- try {
430
- action();
431
- } catch {
432
- }
433
- if (!keepId) {
434
- activeIds.delete(requestId);
435
- }
436
- chunkAbort.abort();
437
- };
438
- const stream = new ReadableStream({
439
- start(controller) {
440
- streamController = controller;
441
- const onMessage = (event) => {
442
- try {
443
- const data = JSON.parse(String(event.data));
444
- if (data.type !== "cf_agent_use_chat_response") {
445
- return;
446
- }
447
- if (data.id !== requestId) {
448
- return;
449
- }
450
- if (data.error) {
451
- finish(() => controller.error(new Error(data.body || "Stream error")));
452
- return;
453
- }
454
- if (data.body?.trim()) {
455
- try {
456
- const chunk = JSON.parse(data.body);
457
- controller.enqueue(chunk);
458
- } catch {
459
- }
460
- }
461
- if (data.done) {
462
- finish(() => controller.close());
463
- }
464
- } catch {
465
- }
466
- };
467
- socket.addEventListener("message", onMessage, { signal: chunkAbort.signal });
468
- },
469
- cancel: () => {
470
- if (this.cancelOnClientAbort) {
471
- this.sendCancelFrame(requestId);
472
- finish(() => {
473
- }, true);
474
- } else {
475
- finish(() => {
476
- }, false, false);
477
- }
478
- }
479
- });
480
- this.setActiveServerTurn(requestId, () => {
481
- if (completed) {
482
- return false;
483
- }
484
- finish(() => streamController.error(abortError), true);
485
- return true;
486
- });
487
- return stream;
488
- }
489
- }
5
+ import { isToolUIPart, getToolName } from 'ai';
6
+ import { ref, watch, onScopeDispose } from 'vue';
490
7
 
491
8
  function isRecord(value) {
492
9
  return typeof value === "object" && value !== null;
493
10
  }
494
- function parseProtocolMessage(data) {
495
- const parsed = JSON.parse(data);
11
+ function parseProtocolMessage(parsed) {
496
12
  if (!isRecord(parsed) || typeof parsed.type !== "string") {
497
13
  return null;
498
14
  }
@@ -508,6 +24,7 @@ function parseProtocolMessage(data) {
508
24
  replayComplete: typeof parsed.replayComplete === "boolean" ? parsed.replayComplete : void 0,
509
25
  continuation: typeof parsed.continuation === "boolean" ? parsed.continuation : void 0,
510
26
  recovering: typeof parsed.recovering === "boolean" ? parsed.recovering : void 0,
27
+ probeId: typeof parsed.probeId === "string" ? parsed.probeId : void 0,
511
28
  message,
512
29
  messages
513
30
  };
@@ -604,10 +121,20 @@ function preserveActiveStreamMessages(currentMessages, incomingMessages) {
604
121
  return nextMessages;
605
122
  }
606
123
  }
607
- return [...incomingMessages, tailMessage];
124
+ return incomingMessages;
125
+ }
126
+ const CONTINUATION_BLOCKING_STATUSES = /* @__PURE__ */ new Set(["submitted", "streaming", "error"]);
127
+ function mergeById(onScreen, stored) {
128
+ const storedIds = new Set(stored.map((message) => message.id));
129
+ return [...stored, ...onScreen.filter((message) => !storedIds.has(message.id))];
608
130
  }
609
131
  function createMessageId() {
610
- return globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);
132
+ if (globalThis.crypto?.randomUUID) {
133
+ return globalThis.crypto.randomUUID();
134
+ }
135
+ const bytes = new Uint8Array(16);
136
+ globalThis.crypto.getRandomValues(bytes);
137
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
611
138
  }
612
139
  function useAgentChat(options) {
613
140
  const autoContinueAfterToolResult = options.autoContinueAfterToolResult ?? true;
@@ -615,19 +142,30 @@ function useAgentChat(options) {
615
142
  const fallbackAckedResumeRequestIds = /* @__PURE__ */ new Set();
616
143
  let streamState = { status: "idle" };
617
144
  let isResumingToolContinuation = false;
145
+ let pendingToolContinuation = false;
146
+ let continuationLaunchTimer = null;
147
+ let observedToolContinuationRequestId = null;
148
+ const dispatchedToolCallIds = /* @__PURE__ */ new Set();
618
149
  let resumeInFlight = false;
619
150
  let resumeGeneration = 0;
620
151
  const isRecovering = ref(false);
621
152
  const connectionError = ref(null);
153
+ const connectionState = ref("connected");
154
+ let resyncPending = false;
155
+ const isServerStreaming = ref(false);
622
156
  const socket = new AgentClient({
623
157
  ...options.client,
624
158
  onConnectionError: (error) => {
625
159
  connectionError.value = error;
626
160
  options.client.onConnectionError?.(error);
161
+ },
162
+ onStateUpdate: (state, source) => {
163
+ options.onStateUpdate?.(state, source);
164
+ options.client.onStateUpdate?.(state, source);
627
165
  }
628
166
  });
629
167
  const transport = new WebSocketChatTransport({
630
- socket,
168
+ agent: socket,
631
169
  activeRequestIds,
632
170
  cancelOnClientAbort: options.cancelOnClientAbort ?? false,
633
171
  prepareBody: async () => {
@@ -637,7 +175,7 @@ function useAgentChat(options) {
637
175
  return clientTools ? { ...extraBody, clientTools } : extraBody;
638
176
  }
639
177
  });
640
- const handleToolCall = async ({ toolCall }) => {
178
+ async function dispatchToolCall(toolCall) {
641
179
  const clientTool = options.tools?.[toolCall.toolName];
642
180
  const addClientToolOutput = (toolOutput) => addToolOutputForTool(toolCall.toolName, toolOutput);
643
181
  if (options.onToolCall) {
@@ -657,23 +195,69 @@ function useAgentChat(options) {
657
195
  errorText: getErrorText(error)
658
196
  });
659
197
  }
660
- };
198
+ }
661
199
  const chat = new Chat({
662
200
  messages: options.initialMessages,
663
201
  transport,
664
- onToolCall: handleToolCall,
665
202
  onError: options.onError
666
203
  });
667
- function startToolContinuation() {
668
- if (!autoContinueAfterToolResult || isResumingToolContinuation) {
204
+ function dispatchRenderedToolCalls() {
205
+ if (chat.status === "streaming" || chat.status === "submitted" || isServerStreaming.value || isRecovering.value) {
206
+ return;
207
+ }
208
+ const lastMessage = chat.messages.at(-1);
209
+ if (!lastMessage || lastMessage.role !== "assistant") {
669
210
  return;
670
211
  }
212
+ for (const part of lastMessage.parts) {
213
+ if (!isToolUIPart(part) || part.state !== "input-available" || dispatchedToolCallIds.has(part.toolCallId)) {
214
+ continue;
215
+ }
216
+ dispatchedToolCallIds.add(part.toolCallId);
217
+ void dispatchToolCall({ toolCallId: part.toolCallId, toolName: getToolName(part), input: part.input });
218
+ }
219
+ }
220
+ function armToolContinuation() {
221
+ if (!autoContinueAfterToolResult || isResumingToolContinuation) {
222
+ return false;
223
+ }
671
224
  isResumingToolContinuation = true;
672
- transport.expectToolContinuation();
673
- void chat.resumeStream().finally(() => {
674
- isResumingToolContinuation = false;
675
- });
225
+ pendingToolContinuation = true;
226
+ return true;
227
+ }
228
+ function scheduleToolContinuationLaunch() {
229
+ if (!pendingToolContinuation || continuationLaunchTimer !== null || CONTINUATION_BLOCKING_STATUSES.has(chat.status)) {
230
+ return;
231
+ }
232
+ continuationLaunchTimer = setTimeout(() => {
233
+ continuationLaunchTimer = null;
234
+ if (!pendingToolContinuation || CONTINUATION_BLOCKING_STATUSES.has(chat.status)) {
235
+ return;
236
+ }
237
+ pendingToolContinuation = false;
238
+ transport.expectToolContinuation();
239
+ void chat.resumeStream().finally(() => {
240
+ isResumingToolContinuation = false;
241
+ });
242
+ }, 0);
243
+ }
244
+ function resetToolContinuation() {
245
+ pendingToolContinuation = false;
246
+ isResumingToolContinuation = false;
247
+ observedToolContinuationRequestId = null;
248
+ if (continuationLaunchTimer !== null) {
249
+ clearTimeout(continuationLaunchTimer);
250
+ continuationLaunchTimer = null;
251
+ }
676
252
  }
253
+ watch([() => chat.messages, () => chat.status, isServerStreaming, isRecovering], () => {
254
+ if (chat.status === "error" && pendingToolContinuation) {
255
+ resetToolContinuation();
256
+ } else {
257
+ scheduleToolContinuationLaunch();
258
+ }
259
+ dispatchRenderedToolCalls();
260
+ });
677
261
  async function stop() {
678
262
  try {
679
263
  transport.cancelActiveServerTurn();
@@ -684,8 +268,9 @@ function useAgentChat(options) {
684
268
  }
685
269
  function clearHistory() {
686
270
  void stop();
687
- isResumingToolContinuation = false;
271
+ resetToolContinuation();
688
272
  isRecovering.value = false;
273
+ isServerStreaming.value = false;
689
274
  streamState = broadcastTransition(streamState, { type: "clear" }).state;
690
275
  chat.messages = [];
691
276
  socket.send(JSON.stringify({ type: "cf_agent_chat_clear" }));
@@ -693,6 +278,7 @@ function useAgentChat(options) {
693
278
  async function addToolOutputForTool(toolName, toolOutput) {
694
279
  const shouldAutoContinue = toolOutput.state === "output-error" ? false : autoContinueAfterToolResult;
695
280
  const clientTools = getClientToolSchemas(options.tools);
281
+ const continuing = shouldAutoContinue && armToolContinuation();
696
282
  socket.send(
697
283
  JSON.stringify({
698
284
  type: "cf_agent_tool_result",
@@ -719,8 +305,8 @@ function useAgentChat(options) {
719
305
  output: toolOutput.output
720
306
  });
721
307
  }
722
- if (shouldAutoContinue) {
723
- startToolContinuation();
308
+ if (continuing) {
309
+ scheduleToolContinuationLaunch();
724
310
  }
725
311
  }
726
312
  async function addToolOutput(...args) {
@@ -734,6 +320,7 @@ function useAgentChat(options) {
734
320
  console.warn(`[agents-vue] Could not find tool call for approval "${approvalResponse.id}".`);
735
321
  return;
736
322
  }
323
+ const continuing = armToolContinuation();
737
324
  socket.send(
738
325
  JSON.stringify({
739
326
  type: "cf_agent_tool_approval",
@@ -743,11 +330,17 @@ function useAgentChat(options) {
743
330
  })
744
331
  );
745
332
  await chat.addToolApprovalResponse(...args);
746
- startToolContinuation();
333
+ if (continuing) {
334
+ scheduleToolContinuationLaunch();
335
+ }
747
336
  }
748
337
  socket.addEventListener("message", (event) => {
749
338
  try {
750
- const data = parseProtocolMessage(String(event.data));
339
+ const raw = JSON.parse(String(event.data));
340
+ if (options.onFrame?.(raw) === true) {
341
+ return;
342
+ }
343
+ const data = parseProtocolMessage(raw);
751
344
  if (!data) {
752
345
  return;
753
346
  }
@@ -767,24 +360,42 @@ function useAgentChat(options) {
767
360
  streamState = broadcastTransition(streamState, { type: "clear" }).state;
768
361
  chat.messages = [];
769
362
  isRecovering.value = false;
363
+ isServerStreaming.value = false;
770
364
  break;
771
365
  // Forward resume handshake to transport
772
- case "cf_agent_stream_resuming":
773
- if (data.id && !transport.handleStreamResuming({ type: data.type, id: data.id }) && !activeRequestIds.has(data.id) && !fallbackAckedResumeRequestIds.has(data.id)) {
774
- streamState = broadcastTransition(streamState, {
366
+ case "cf_agent_stream_resuming": {
367
+ const isEarlyToolContinuation = isResumingToolContinuation && !transport.isAwaitingResume();
368
+ if (data.id && !transport.handleStreamResuming({ id: data.id }) && !activeRequestIds.has(data.id) && !fallbackAckedResumeRequestIds.has(data.id)) {
369
+ if (isEarlyToolContinuation) {
370
+ pendingToolContinuation = false;
371
+ observedToolContinuationRequestId = data.id;
372
+ if (continuationLaunchTimer !== null) {
373
+ clearTimeout(continuationLaunchTimer);
374
+ continuationLaunchTimer = null;
375
+ }
376
+ }
377
+ const resumed = broadcastTransition(streamState, {
775
378
  type: "resume-fallback",
776
379
  streamId: data.id,
777
380
  messageId: createMessageId()
778
- }).state;
381
+ });
382
+ streamState = resumed.state;
383
+ isServerStreaming.value = resumed.isStreaming;
779
384
  fallbackAckedResumeRequestIds.add(data.id);
780
385
  transport.observeServerTurn(data.id);
781
386
  isRecovering.value = false;
782
387
  socket.send(JSON.stringify({ type: "cf_agent_stream_resume_ack", id: data.id }));
783
388
  }
784
389
  break;
785
- case "cf_agent_stream_resume_none":
786
- transport.handleStreamResumeNone();
390
+ }
391
+ // 0.23 carries the probe id, so a NONE answering an older probe cannot settle this one.
392
+ case "cf_agent_stream_resume_none": {
393
+ const settled = transport.handleStreamResumeNone({ probeId: data.probeId });
394
+ if (settled && observedToolContinuationRequestId !== null) {
395
+ resetToolContinuation();
396
+ }
787
397
  break;
398
+ }
788
399
  // Pre-stream window (0.9.0): keep the in-flight resume probe waiting.
789
400
  case "cf_agent_stream_pending":
790
401
  transport.handleStreamPending();
@@ -798,6 +409,9 @@ function useAgentChat(options) {
798
409
  return;
799
410
  }
800
411
  {
412
+ if (!data.done && !data.error && transport.activeServerTurnId !== data.id) {
413
+ transport.observeServerTurn(data.id);
414
+ }
801
415
  const chunkData = data.body?.trim() ? JSON.parse(data.body) : void 0;
802
416
  const result = broadcastTransition(streamState, {
803
417
  type: "response",
@@ -812,12 +426,16 @@ function useAgentChat(options) {
812
426
  currentMessages: data.continuation ? chat.messages : void 0
813
427
  });
814
428
  streamState = result.state;
429
+ isServerStreaming.value = result.isStreaming;
815
430
  if (result.messagesUpdate) {
816
431
  chat.messages = result.messagesUpdate(chat.messages);
817
432
  }
818
- if (data.done) {
433
+ if (data.done || data.error) {
819
434
  isRecovering.value = false;
820
435
  transport.handleServerTurnCompleted(data.id);
436
+ if (observedToolContinuationRequestId === data.id) {
437
+ resetToolContinuation();
438
+ }
821
439
  }
822
440
  }
823
441
  break;
@@ -827,8 +445,18 @@ function useAgentChat(options) {
827
445
  });
828
446
  socket.addEventListener("open", (event) => {
829
447
  connectionError.value = null;
448
+ connectionState.value = "connected";
830
449
  options.onOpen?.(event);
831
- if (options.resume !== false && !resumeInFlight && !isResumingToolContinuation && !transport.isAwaitingResume()) {
450
+ void seedInitialMessages();
451
+ if (resyncPending) {
452
+ resyncPending = false;
453
+ void resyncMessages();
454
+ }
455
+ if (transport.retryPendingResume()) {
456
+ return;
457
+ }
458
+ const canReconcileObservedContinuation = observedToolContinuationRequestId !== null;
459
+ if (options.resume !== false && !resumeInFlight && (!isResumingToolContinuation || canReconcileObservedContinuation) && !transport.isAwaitingResume()) {
832
460
  resumeInFlight = true;
833
461
  const myGeneration = resumeGeneration;
834
462
  void chat.resumeStream().catch(() => {
@@ -843,6 +471,8 @@ function useAgentChat(options) {
843
471
  socket.addEventListener("close", (event) => {
844
472
  fallbackAckedResumeRequestIds.clear();
845
473
  isRecovering.value = false;
474
+ connectionState.value = "reconnecting";
475
+ resyncPending = true;
846
476
  if (options.onClose && event instanceof CloseEvent) {
847
477
  options.onClose(event);
848
478
  }
@@ -852,23 +482,73 @@ function useAgentChat(options) {
852
482
  });
853
483
  onScopeDispose(() => {
854
484
  fallbackAckedResumeRequestIds.clear();
485
+ resetToolContinuation();
855
486
  isRecovering.value = false;
856
487
  resumeGeneration++;
857
488
  resumeInFlight = false;
858
489
  socket.close();
859
490
  });
491
+ function setAgent(next) {
492
+ transport.setAgent(next);
493
+ }
494
+ const initialMessagesLoaded = ref(false);
495
+ let initialMessagesSeedStarted = options.initialMessages !== void 0;
496
+ initialMessagesLoaded.value = initialMessagesSeedStarted;
497
+ async function fetchStoredMessages() {
498
+ try {
499
+ const target = new URL(socket.url);
500
+ target.protocol = target.protocol === "wss:" ? "https:" : "http:";
501
+ target.searchParams.delete("_pk");
502
+ target.pathname += "/get-messages";
503
+ const response = await fetch(target.toString(), { credentials: "include" });
504
+ if (!response.ok) {
505
+ console.warn(`[agents-vue] get-messages failed: ${response.status} ${response.statusText}`);
506
+ return null;
507
+ }
508
+ const text = await response.text();
509
+ return text.trim() ? JSON.parse(text) : null;
510
+ } catch (error) {
511
+ console.warn("[agents-vue] get-messages errored", error);
512
+ return null;
513
+ }
514
+ }
515
+ async function seedInitialMessages() {
516
+ if (initialMessagesSeedStarted) {
517
+ return;
518
+ }
519
+ initialMessagesSeedStarted = true;
520
+ try {
521
+ const stored = await fetchStoredMessages();
522
+ if (stored && chat.messages.length === 0) {
523
+ chat.messages = stored;
524
+ }
525
+ } finally {
526
+ initialMessagesLoaded.value = true;
527
+ }
528
+ }
529
+ async function resyncMessages() {
530
+ const stored = await fetchStoredMessages();
531
+ if (stored) {
532
+ chat.messages = (options.mergeMessages ?? mergeById)(chat.messages, stored);
533
+ }
534
+ }
860
535
  return {
861
536
  addToolApprovalResponse,
862
537
  addToolOutput,
863
538
  clearHistory,
864
539
  chat,
865
540
  socket,
541
+ setAgent,
866
542
  stop,
543
+ /** Reactive: true once the `get-messages` seed has settled (or was skipped). */
544
+ initialMessagesLoaded,
867
545
  /** Reactive: true while the server is recovering a durable chat turn. */
868
546
  isRecovering,
869
547
  /** Reactive: terminal WebSocket connection failure, or null. */
870
- connectionError
548
+ connectionError,
549
+ /** Reactive: whether the socket is up, for the header's "Reconnecting…". */
550
+ connectionState
871
551
  };
872
552
  }
873
553
 
874
- export { WebSocketChatTransport, useAgentChat };
554
+ export { useAgentChat };