agents-vue 0.2.1 → 0.4.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,31 @@ 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))];
130
+ }
131
+ function idsOf(messages) {
132
+ return messages.map((message) => message.id);
133
+ }
134
+ function startMessageIdOf(body) {
135
+ try {
136
+ const chunk = JSON.parse(body);
137
+ return isRecord(chunk) && chunk.type === "start" && typeof chunk.messageId === "string" ? chunk.messageId : null;
138
+ } catch {
139
+ return null;
140
+ }
608
141
  }
609
142
  function createMessageId() {
610
- return globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);
143
+ if (globalThis.crypto?.randomUUID) {
144
+ return globalThis.crypto.randomUUID();
145
+ }
146
+ const bytes = new Uint8Array(16);
147
+ globalThis.crypto.getRandomValues(bytes);
148
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
611
149
  }
612
150
  function useAgentChat(options) {
613
151
  const autoContinueAfterToolResult = options.autoContinueAfterToolResult ?? true;
@@ -615,19 +153,32 @@ function useAgentChat(options) {
615
153
  const fallbackAckedResumeRequestIds = /* @__PURE__ */ new Set();
616
154
  let streamState = { status: "idle" };
617
155
  let isResumingToolContinuation = false;
156
+ let pendingToolContinuation = false;
157
+ let continuationLaunchTimer = null;
158
+ let observedToolContinuationRequestId = null;
159
+ const dispatchedToolCallIds = /* @__PURE__ */ new Set();
160
+ let heldMessageIds = idsOf(options.initialMessages ?? []);
161
+ const streamMessageIds = /* @__PURE__ */ new Map();
618
162
  let resumeInFlight = false;
619
163
  let resumeGeneration = 0;
620
164
  const isRecovering = ref(false);
621
165
  const connectionError = ref(null);
166
+ const connectionState = ref("connected");
167
+ let resyncPending = false;
168
+ const isServerStreaming = ref(false);
622
169
  const socket = new AgentClient({
623
170
  ...options.client,
624
171
  onConnectionError: (error) => {
625
172
  connectionError.value = error;
626
173
  options.client.onConnectionError?.(error);
174
+ },
175
+ onStateUpdate: (state, source) => {
176
+ options.onStateUpdate?.(state, source);
177
+ options.client.onStateUpdate?.(state, source);
627
178
  }
628
179
  });
629
180
  const transport = new WebSocketChatTransport({
630
- socket,
181
+ agent: socket,
631
182
  activeRequestIds,
632
183
  cancelOnClientAbort: options.cancelOnClientAbort ?? false,
633
184
  prepareBody: async () => {
@@ -637,7 +188,7 @@ function useAgentChat(options) {
637
188
  return clientTools ? { ...extraBody, clientTools } : extraBody;
638
189
  }
639
190
  });
640
- const handleToolCall = async ({ toolCall }) => {
191
+ async function dispatchToolCall(toolCall) {
641
192
  const clientTool = options.tools?.[toolCall.toolName];
642
193
  const addClientToolOutput = (toolOutput) => addToolOutputForTool(toolCall.toolName, toolOutput);
643
194
  if (options.onToolCall) {
@@ -657,23 +208,110 @@ function useAgentChat(options) {
657
208
  errorText: getErrorText(error)
658
209
  });
659
210
  }
660
- };
211
+ }
661
212
  const chat = new Chat({
662
213
  messages: options.initialMessages,
663
- transport,
664
- onToolCall: handleToolCall,
214
+ transport: options.sendOnlyNewMessages ? {
215
+ sendMessages: (request) => transport.sendMessages(onlyNewMessages(request)),
216
+ reconnectToStream: (request) => transport.reconnectToStream(request)
217
+ } : transport,
665
218
  onError: options.onError
666
219
  });
667
- function startToolContinuation() {
668
- if (!autoContinueAfterToolResult || isResumingToolContinuation) {
220
+ function onlyNewMessages(request) {
221
+ if (request.trigger === "regenerate-message") {
222
+ return request;
223
+ }
224
+ const { messages } = request;
225
+ let held = 0;
226
+ while (held < messages.length && messages[held]?.id === heldMessageIds[held]) {
227
+ held++;
228
+ }
229
+ const rewrittenIndex = messages.findIndex((message) => message.id === request.messageId);
230
+ const start = rewrittenIndex >= 0 ? Math.min(held, rewrittenIndex) : held;
231
+ if (rewrittenIndex >= 0) {
232
+ heldMessageIds = heldMessageIds.slice(0, start);
233
+ }
234
+ const afterMessageId = messages[start - 1]?.id;
235
+ if (afterMessageId === void 0 || start === messages.length) {
236
+ return request;
237
+ }
238
+ return { ...request, messages: messages.slice(start), body: { ...request.body, afterMessageId } };
239
+ }
240
+ function holdThrough(messageId) {
241
+ const index = chat.messages.findIndex((message) => message.id === messageId);
242
+ if (index >= 0) {
243
+ heldMessageIds = idsOf(chat.messages.slice(0, index + 1));
244
+ }
245
+ }
246
+ function trackStreamMessage(requestId, frame) {
247
+ if (frame.done || frame.error) {
248
+ const messageId = streamMessageIds.get(requestId);
249
+ streamMessageIds.delete(requestId);
250
+ const reply = chat.messages.find((message) => message.id === messageId);
251
+ if (reply && reply.parts.length > 0 && !frame.error) {
252
+ holdThrough(reply.id);
253
+ }
254
+ } else if (!streamMessageIds.has(requestId) && frame.body?.trim()) {
255
+ streamMessageIds.set(requestId, startMessageIdOf(frame.body));
256
+ }
257
+ }
258
+ function dispatchRenderedToolCalls() {
259
+ if (chat.status === "streaming" || chat.status === "submitted" || isServerStreaming.value || isRecovering.value) {
669
260
  return;
670
261
  }
262
+ const lastMessage = chat.messages.at(-1);
263
+ if (!lastMessage || lastMessage.role !== "assistant") {
264
+ return;
265
+ }
266
+ for (const part of lastMessage.parts) {
267
+ if (!isToolUIPart(part) || part.state !== "input-available" || dispatchedToolCallIds.has(part.toolCallId)) {
268
+ continue;
269
+ }
270
+ dispatchedToolCallIds.add(part.toolCallId);
271
+ void dispatchToolCall({ toolCallId: part.toolCallId, toolName: getToolName(part), input: part.input });
272
+ }
273
+ }
274
+ function armToolContinuation() {
275
+ if (!autoContinueAfterToolResult || isResumingToolContinuation) {
276
+ return false;
277
+ }
671
278
  isResumingToolContinuation = true;
672
- transport.expectToolContinuation();
673
- void chat.resumeStream().finally(() => {
674
- isResumingToolContinuation = false;
675
- });
279
+ pendingToolContinuation = true;
280
+ return true;
676
281
  }
282
+ function scheduleToolContinuationLaunch() {
283
+ if (!pendingToolContinuation || continuationLaunchTimer !== null || CONTINUATION_BLOCKING_STATUSES.has(chat.status)) {
284
+ return;
285
+ }
286
+ continuationLaunchTimer = setTimeout(() => {
287
+ continuationLaunchTimer = null;
288
+ if (!pendingToolContinuation || CONTINUATION_BLOCKING_STATUSES.has(chat.status)) {
289
+ return;
290
+ }
291
+ pendingToolContinuation = false;
292
+ transport.expectToolContinuation();
293
+ void chat.resumeStream().finally(() => {
294
+ isResumingToolContinuation = false;
295
+ });
296
+ }, 0);
297
+ }
298
+ function resetToolContinuation() {
299
+ pendingToolContinuation = false;
300
+ isResumingToolContinuation = false;
301
+ observedToolContinuationRequestId = null;
302
+ if (continuationLaunchTimer !== null) {
303
+ clearTimeout(continuationLaunchTimer);
304
+ continuationLaunchTimer = null;
305
+ }
306
+ }
307
+ watch([() => chat.messages, () => chat.status, isServerStreaming, isRecovering], () => {
308
+ if (chat.status === "error" && pendingToolContinuation) {
309
+ resetToolContinuation();
310
+ } else {
311
+ scheduleToolContinuationLaunch();
312
+ }
313
+ dispatchRenderedToolCalls();
314
+ });
677
315
  async function stop() {
678
316
  try {
679
317
  transport.cancelActiveServerTurn();
@@ -684,15 +322,18 @@ function useAgentChat(options) {
684
322
  }
685
323
  function clearHistory() {
686
324
  void stop();
687
- isResumingToolContinuation = false;
325
+ resetToolContinuation();
688
326
  isRecovering.value = false;
327
+ isServerStreaming.value = false;
689
328
  streamState = broadcastTransition(streamState, { type: "clear" }).state;
690
329
  chat.messages = [];
330
+ heldMessageIds = [];
691
331
  socket.send(JSON.stringify({ type: "cf_agent_chat_clear" }));
692
332
  }
693
333
  async function addToolOutputForTool(toolName, toolOutput) {
694
334
  const shouldAutoContinue = toolOutput.state === "output-error" ? false : autoContinueAfterToolResult;
695
335
  const clientTools = getClientToolSchemas(options.tools);
336
+ const continuing = shouldAutoContinue && armToolContinuation();
696
337
  socket.send(
697
338
  JSON.stringify({
698
339
  type: "cf_agent_tool_result",
@@ -719,8 +360,8 @@ function useAgentChat(options) {
719
360
  output: toolOutput.output
720
361
  });
721
362
  }
722
- if (shouldAutoContinue) {
723
- startToolContinuation();
363
+ if (continuing) {
364
+ scheduleToolContinuationLaunch();
724
365
  }
725
366
  }
726
367
  async function addToolOutput(...args) {
@@ -734,6 +375,7 @@ function useAgentChat(options) {
734
375
  console.warn(`[agents-vue] Could not find tool call for approval "${approvalResponse.id}".`);
735
376
  return;
736
377
  }
378
+ const continuing = armToolContinuation();
737
379
  socket.send(
738
380
  JSON.stringify({
739
381
  type: "cf_agent_tool_approval",
@@ -743,11 +385,17 @@ function useAgentChat(options) {
743
385
  })
744
386
  );
745
387
  await chat.addToolApprovalResponse(...args);
746
- startToolContinuation();
388
+ if (continuing) {
389
+ scheduleToolContinuationLaunch();
390
+ }
747
391
  }
748
392
  socket.addEventListener("message", (event) => {
749
393
  try {
750
- const data = parseProtocolMessage(String(event.data));
394
+ const raw = JSON.parse(String(event.data));
395
+ if (options.onFrame?.(raw) === true) {
396
+ return;
397
+ }
398
+ const data = parseProtocolMessage(raw);
751
399
  if (!data) {
752
400
  return;
753
401
  }
@@ -755,36 +403,57 @@ function useAgentChat(options) {
755
403
  // Full message list sync (sent on connect, and after every appendMessage / turn).
756
404
  case "cf_agent_chat_messages":
757
405
  chat.messages = chat.status === "streaming" || chat.status === "submitted" || streamState.status !== "idle" ? preserveActiveStreamMessages(chat.messages, data.messages ?? []) : data.messages ?? [];
406
+ heldMessageIds = idsOf(data.messages ?? []);
758
407
  break;
759
408
  // Single message update (e.g. tool result applied)
760
409
  case "cf_agent_message_updated":
761
410
  if (data.message) {
762
411
  chat.messages = replaceUpdatedMessage(chat.messages, data.message);
412
+ holdThrough(data.message.id);
763
413
  }
764
414
  break;
765
415
  // Clear all messages
766
416
  case "cf_agent_chat_clear":
767
417
  streamState = broadcastTransition(streamState, { type: "clear" }).state;
768
418
  chat.messages = [];
419
+ heldMessageIds = [];
769
420
  isRecovering.value = false;
421
+ isServerStreaming.value = false;
770
422
  break;
771
423
  // 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, {
424
+ case "cf_agent_stream_resuming": {
425
+ const isEarlyToolContinuation = isResumingToolContinuation && !transport.isAwaitingResume();
426
+ if (data.id && !transport.handleStreamResuming({ id: data.id }) && !activeRequestIds.has(data.id) && !fallbackAckedResumeRequestIds.has(data.id)) {
427
+ if (isEarlyToolContinuation) {
428
+ pendingToolContinuation = false;
429
+ observedToolContinuationRequestId = data.id;
430
+ if (continuationLaunchTimer !== null) {
431
+ clearTimeout(continuationLaunchTimer);
432
+ continuationLaunchTimer = null;
433
+ }
434
+ }
435
+ const resumed = broadcastTransition(streamState, {
775
436
  type: "resume-fallback",
776
437
  streamId: data.id,
777
438
  messageId: createMessageId()
778
- }).state;
439
+ });
440
+ streamState = resumed.state;
441
+ isServerStreaming.value = resumed.isStreaming;
779
442
  fallbackAckedResumeRequestIds.add(data.id);
780
443
  transport.observeServerTurn(data.id);
781
444
  isRecovering.value = false;
782
445
  socket.send(JSON.stringify({ type: "cf_agent_stream_resume_ack", id: data.id }));
783
446
  }
784
447
  break;
785
- case "cf_agent_stream_resume_none":
786
- transport.handleStreamResumeNone();
448
+ }
449
+ // 0.23 carries the probe id, so a NONE answering an older probe cannot settle this one.
450
+ case "cf_agent_stream_resume_none": {
451
+ const settled = transport.handleStreamResumeNone({ probeId: data.probeId });
452
+ if (settled && observedToolContinuationRequestId !== null) {
453
+ resetToolContinuation();
454
+ }
787
455
  break;
456
+ }
788
457
  // Pre-stream window (0.9.0): keep the in-flight resume probe waiting.
789
458
  case "cf_agent_stream_pending":
790
459
  transport.handleStreamPending();
@@ -794,10 +463,16 @@ function useAgentChat(options) {
794
463
  isRecovering.value = Boolean(data.recovering);
795
464
  break;
796
465
  case "cf_agent_use_chat_response":
466
+ if (data.id) {
467
+ trackStreamMessage(data.id, data);
468
+ }
797
469
  if (!data.id || activeRequestIds.has(data.id)) {
798
470
  return;
799
471
  }
800
472
  {
473
+ if (!data.done && !data.error && transport.activeServerTurnId !== data.id) {
474
+ transport.observeServerTurn(data.id);
475
+ }
801
476
  const chunkData = data.body?.trim() ? JSON.parse(data.body) : void 0;
802
477
  const result = broadcastTransition(streamState, {
803
478
  type: "response",
@@ -812,12 +487,16 @@ function useAgentChat(options) {
812
487
  currentMessages: data.continuation ? chat.messages : void 0
813
488
  });
814
489
  streamState = result.state;
490
+ isServerStreaming.value = result.isStreaming;
815
491
  if (result.messagesUpdate) {
816
492
  chat.messages = result.messagesUpdate(chat.messages);
817
493
  }
818
- if (data.done) {
494
+ if (data.done || data.error) {
819
495
  isRecovering.value = false;
820
496
  transport.handleServerTurnCompleted(data.id);
497
+ if (observedToolContinuationRequestId === data.id) {
498
+ resetToolContinuation();
499
+ }
821
500
  }
822
501
  }
823
502
  break;
@@ -827,8 +506,18 @@ function useAgentChat(options) {
827
506
  });
828
507
  socket.addEventListener("open", (event) => {
829
508
  connectionError.value = null;
509
+ connectionState.value = "connected";
830
510
  options.onOpen?.(event);
831
- if (options.resume !== false && !resumeInFlight && !isResumingToolContinuation && !transport.isAwaitingResume()) {
511
+ void seedInitialMessages();
512
+ if (resyncPending) {
513
+ resyncPending = false;
514
+ void resyncMessages();
515
+ }
516
+ if (transport.retryPendingResume()) {
517
+ return;
518
+ }
519
+ const canReconcileObservedContinuation = observedToolContinuationRequestId !== null;
520
+ if (options.resume !== false && !resumeInFlight && (!isResumingToolContinuation || canReconcileObservedContinuation) && !transport.isAwaitingResume()) {
832
521
  resumeInFlight = true;
833
522
  const myGeneration = resumeGeneration;
834
523
  void chat.resumeStream().catch(() => {
@@ -842,7 +531,10 @@ function useAgentChat(options) {
842
531
  });
843
532
  socket.addEventListener("close", (event) => {
844
533
  fallbackAckedResumeRequestIds.clear();
534
+ streamMessageIds.clear();
845
535
  isRecovering.value = false;
536
+ connectionState.value = "reconnecting";
537
+ resyncPending = true;
846
538
  if (options.onClose && event instanceof CloseEvent) {
847
539
  options.onClose(event);
848
540
  }
@@ -852,23 +544,75 @@ function useAgentChat(options) {
852
544
  });
853
545
  onScopeDispose(() => {
854
546
  fallbackAckedResumeRequestIds.clear();
547
+ resetToolContinuation();
855
548
  isRecovering.value = false;
856
549
  resumeGeneration++;
857
550
  resumeInFlight = false;
858
551
  socket.close();
859
552
  });
553
+ function setAgent(next) {
554
+ transport.setAgent(next);
555
+ }
556
+ const initialMessagesLoaded = ref(false);
557
+ let initialMessagesSeedStarted = options.initialMessages !== void 0;
558
+ initialMessagesLoaded.value = initialMessagesSeedStarted;
559
+ async function fetchStoredMessages() {
560
+ try {
561
+ const target = new URL(socket.url);
562
+ target.protocol = target.protocol === "wss:" ? "https:" : "http:";
563
+ target.searchParams.delete("_pk");
564
+ target.pathname += "/get-messages";
565
+ const response = await fetch(target.toString(), { credentials: "include" });
566
+ if (!response.ok) {
567
+ console.warn(`[agents-vue] get-messages failed: ${response.status} ${response.statusText}`);
568
+ return null;
569
+ }
570
+ const text = await response.text();
571
+ return text.trim() ? JSON.parse(text) : null;
572
+ } catch (error) {
573
+ console.warn("[agents-vue] get-messages errored", error);
574
+ return null;
575
+ }
576
+ }
577
+ async function seedInitialMessages() {
578
+ if (initialMessagesSeedStarted) {
579
+ return;
580
+ }
581
+ initialMessagesSeedStarted = true;
582
+ try {
583
+ const stored = await fetchStoredMessages();
584
+ if (stored && chat.messages.length === 0) {
585
+ chat.messages = stored;
586
+ heldMessageIds = idsOf(stored);
587
+ }
588
+ } finally {
589
+ initialMessagesLoaded.value = true;
590
+ }
591
+ }
592
+ async function resyncMessages() {
593
+ const stored = await fetchStoredMessages();
594
+ if (stored) {
595
+ chat.messages = (options.mergeMessages ?? mergeById)(chat.messages, stored);
596
+ heldMessageIds = idsOf(stored);
597
+ }
598
+ }
860
599
  return {
861
600
  addToolApprovalResponse,
862
601
  addToolOutput,
863
602
  clearHistory,
864
603
  chat,
865
604
  socket,
605
+ setAgent,
866
606
  stop,
607
+ /** Reactive: true once the `get-messages` seed has settled (or was skipped). */
608
+ initialMessagesLoaded,
867
609
  /** Reactive: true while the server is recovering a durable chat turn. */
868
610
  isRecovering,
869
611
  /** Reactive: terminal WebSocket connection failure, or null. */
870
- connectionError
612
+ connectionError,
613
+ /** Reactive: whether the socket is up, for the header's "Reconnecting…". */
614
+ connectionState
871
615
  };
872
616
  }
873
617
 
874
- export { WebSocketChatTransport, useAgentChat };
618
+ export { useAgentChat };