agents-vue 0.2.0 → 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,17 +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();
149
+ let resumeInFlight = false;
150
+ let resumeGeneration = 0;
618
151
  const isRecovering = ref(false);
619
152
  const connectionError = ref(null);
153
+ const connectionState = ref("connected");
154
+ let resyncPending = false;
155
+ const isServerStreaming = ref(false);
620
156
  const socket = new AgentClient({
621
157
  ...options.client,
622
158
  onConnectionError: (error) => {
623
159
  connectionError.value = error;
624
160
  options.client.onConnectionError?.(error);
161
+ },
162
+ onStateUpdate: (state, source) => {
163
+ options.onStateUpdate?.(state, source);
164
+ options.client.onStateUpdate?.(state, source);
625
165
  }
626
166
  });
627
167
  const transport = new WebSocketChatTransport({
628
- socket,
168
+ agent: socket,
629
169
  activeRequestIds,
630
170
  cancelOnClientAbort: options.cancelOnClientAbort ?? false,
631
171
  prepareBody: async () => {
@@ -635,7 +175,7 @@ function useAgentChat(options) {
635
175
  return clientTools ? { ...extraBody, clientTools } : extraBody;
636
176
  }
637
177
  });
638
- const handleToolCall = async ({ toolCall }) => {
178
+ async function dispatchToolCall(toolCall) {
639
179
  const clientTool = options.tools?.[toolCall.toolName];
640
180
  const addClientToolOutput = (toolOutput) => addToolOutputForTool(toolCall.toolName, toolOutput);
641
181
  if (options.onToolCall) {
@@ -655,23 +195,69 @@ function useAgentChat(options) {
655
195
  errorText: getErrorText(error)
656
196
  });
657
197
  }
658
- };
198
+ }
659
199
  const chat = new Chat({
660
200
  messages: options.initialMessages,
661
201
  transport,
662
- onToolCall: handleToolCall,
663
202
  onError: options.onError
664
203
  });
665
- function startToolContinuation() {
666
- 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") {
667
210
  return;
668
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
+ }
669
224
  isResumingToolContinuation = true;
670
- transport.expectToolContinuation();
671
- void chat.resumeStream().finally(() => {
672
- isResumingToolContinuation = false;
673
- });
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
+ }
674
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
+ });
675
261
  async function stop() {
676
262
  try {
677
263
  transport.cancelActiveServerTurn();
@@ -682,8 +268,9 @@ function useAgentChat(options) {
682
268
  }
683
269
  function clearHistory() {
684
270
  void stop();
685
- isResumingToolContinuation = false;
271
+ resetToolContinuation();
686
272
  isRecovering.value = false;
273
+ isServerStreaming.value = false;
687
274
  streamState = broadcastTransition(streamState, { type: "clear" }).state;
688
275
  chat.messages = [];
689
276
  socket.send(JSON.stringify({ type: "cf_agent_chat_clear" }));
@@ -691,6 +278,7 @@ function useAgentChat(options) {
691
278
  async function addToolOutputForTool(toolName, toolOutput) {
692
279
  const shouldAutoContinue = toolOutput.state === "output-error" ? false : autoContinueAfterToolResult;
693
280
  const clientTools = getClientToolSchemas(options.tools);
281
+ const continuing = shouldAutoContinue && armToolContinuation();
694
282
  socket.send(
695
283
  JSON.stringify({
696
284
  type: "cf_agent_tool_result",
@@ -717,8 +305,8 @@ function useAgentChat(options) {
717
305
  output: toolOutput.output
718
306
  });
719
307
  }
720
- if (shouldAutoContinue) {
721
- startToolContinuation();
308
+ if (continuing) {
309
+ scheduleToolContinuationLaunch();
722
310
  }
723
311
  }
724
312
  async function addToolOutput(...args) {
@@ -732,6 +320,7 @@ function useAgentChat(options) {
732
320
  console.warn(`[agents-vue] Could not find tool call for approval "${approvalResponse.id}".`);
733
321
  return;
734
322
  }
323
+ const continuing = armToolContinuation();
735
324
  socket.send(
736
325
  JSON.stringify({
737
326
  type: "cf_agent_tool_approval",
@@ -741,11 +330,17 @@ function useAgentChat(options) {
741
330
  })
742
331
  );
743
332
  await chat.addToolApprovalResponse(...args);
744
- startToolContinuation();
333
+ if (continuing) {
334
+ scheduleToolContinuationLaunch();
335
+ }
745
336
  }
746
337
  socket.addEventListener("message", (event) => {
747
338
  try {
748
- 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);
749
344
  if (!data) {
750
345
  return;
751
346
  }
@@ -765,24 +360,42 @@ function useAgentChat(options) {
765
360
  streamState = broadcastTransition(streamState, { type: "clear" }).state;
766
361
  chat.messages = [];
767
362
  isRecovering.value = false;
363
+ isServerStreaming.value = false;
768
364
  break;
769
365
  // Forward resume handshake to transport
770
- case "cf_agent_stream_resuming":
771
- if (data.id && !transport.handleStreamResuming({ type: data.type, id: data.id }) && !activeRequestIds.has(data.id) && !fallbackAckedResumeRequestIds.has(data.id)) {
772
- 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, {
773
378
  type: "resume-fallback",
774
379
  streamId: data.id,
775
380
  messageId: createMessageId()
776
- }).state;
381
+ });
382
+ streamState = resumed.state;
383
+ isServerStreaming.value = resumed.isStreaming;
777
384
  fallbackAckedResumeRequestIds.add(data.id);
778
385
  transport.observeServerTurn(data.id);
779
386
  isRecovering.value = false;
780
387
  socket.send(JSON.stringify({ type: "cf_agent_stream_resume_ack", id: data.id }));
781
388
  }
782
389
  break;
783
- case "cf_agent_stream_resume_none":
784
- 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
+ }
785
397
  break;
398
+ }
786
399
  // Pre-stream window (0.9.0): keep the in-flight resume probe waiting.
787
400
  case "cf_agent_stream_pending":
788
401
  transport.handleStreamPending();
@@ -796,6 +409,9 @@ function useAgentChat(options) {
796
409
  return;
797
410
  }
798
411
  {
412
+ if (!data.done && !data.error && transport.activeServerTurnId !== data.id) {
413
+ transport.observeServerTurn(data.id);
414
+ }
799
415
  const chunkData = data.body?.trim() ? JSON.parse(data.body) : void 0;
800
416
  const result = broadcastTransition(streamState, {
801
417
  type: "response",
@@ -810,12 +426,16 @@ function useAgentChat(options) {
810
426
  currentMessages: data.continuation ? chat.messages : void 0
811
427
  });
812
428
  streamState = result.state;
429
+ isServerStreaming.value = result.isStreaming;
813
430
  if (result.messagesUpdate) {
814
431
  chat.messages = result.messagesUpdate(chat.messages);
815
432
  }
816
- if (data.done) {
433
+ if (data.done || data.error) {
817
434
  isRecovering.value = false;
818
435
  transport.handleServerTurnCompleted(data.id);
436
+ if (observedToolContinuationRequestId === data.id) {
437
+ resetToolContinuation();
438
+ }
819
439
  }
820
440
  }
821
441
  break;
@@ -825,14 +445,34 @@ function useAgentChat(options) {
825
445
  });
826
446
  socket.addEventListener("open", (event) => {
827
447
  connectionError.value = null;
448
+ connectionState.value = "connected";
828
449
  options.onOpen?.(event);
829
- if (options.resume !== false) {
830
- void chat.resumeStream();
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()) {
460
+ resumeInFlight = true;
461
+ const myGeneration = resumeGeneration;
462
+ void chat.resumeStream().catch(() => {
463
+ }).finally(() => {
464
+ if (resumeGeneration !== myGeneration) {
465
+ return;
466
+ }
467
+ resumeInFlight = false;
468
+ });
831
469
  }
832
470
  });
833
471
  socket.addEventListener("close", (event) => {
834
472
  fallbackAckedResumeRequestIds.clear();
835
473
  isRecovering.value = false;
474
+ connectionState.value = "reconnecting";
475
+ resyncPending = true;
836
476
  if (options.onClose && event instanceof CloseEvent) {
837
477
  options.onClose(event);
838
478
  }
@@ -842,21 +482,73 @@ function useAgentChat(options) {
842
482
  });
843
483
  onScopeDispose(() => {
844
484
  fallbackAckedResumeRequestIds.clear();
485
+ resetToolContinuation();
845
486
  isRecovering.value = false;
487
+ resumeGeneration++;
488
+ resumeInFlight = false;
846
489
  socket.close();
847
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
+ }
848
535
  return {
849
536
  addToolApprovalResponse,
850
537
  addToolOutput,
851
538
  clearHistory,
852
539
  chat,
853
540
  socket,
541
+ setAgent,
854
542
  stop,
543
+ /** Reactive: true once the `get-messages` seed has settled (or was skipped). */
544
+ initialMessagesLoaded,
855
545
  /** Reactive: true while the server is recovering a durable chat turn. */
856
546
  isRecovering,
857
547
  /** Reactive: terminal WebSocket connection failure, or null. */
858
- connectionError
548
+ connectionError,
549
+ /** Reactive: whether the socket is up, for the header's "Reconnecting…". */
550
+ connectionState
859
551
  };
860
552
  }
861
553
 
862
- export { WebSocketChatTransport, useAgentChat };
554
+ export { useAgentChat };