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/CHANGELOG.md +37 -0
- package/README.md +77 -12
- package/dist/index.d.mts +37 -95
- package/dist/index.d.ts +37 -95
- package/dist/index.mjs +208 -516
- 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,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
|
-
|
|
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
|
|
666
|
-
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") {
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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 (
|
|
721
|
-
|
|
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
|
-
|
|
333
|
+
if (continuing) {
|
|
334
|
+
scheduleToolContinuationLaunch();
|
|
335
|
+
}
|
|
745
336
|
}
|
|
746
337
|
socket.addEventListener("message", (event) => {
|
|
747
338
|
try {
|
|
748
|
-
const
|
|
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
|
-
|
|
772
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
784
|
-
|
|
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
|
-
|
|
830
|
-
|
|
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 {
|
|
554
|
+
export { useAgentChat };
|