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/CHANGELOG.md +25 -0
- package/README.md +76 -12
- package/dist/index.d.mts +37 -95
- package/dist/index.d.ts +37 -95
- package/dist/index.mjs +195 -515
- package/package.json +8 -8
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 {
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
668
|
-
if (
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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 (
|
|
723
|
-
|
|
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
|
-
|
|
333
|
+
if (continuing) {
|
|
334
|
+
scheduleToolContinuationLaunch();
|
|
335
|
+
}
|
|
747
336
|
}
|
|
748
337
|
socket.addEventListener("message", (event) => {
|
|
749
338
|
try {
|
|
750
|
-
const
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
786
|
-
|
|
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
|
-
|
|
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 {
|
|
554
|
+
export { useAgentChat };
|