@firtoz/chat-agent 2.0.0 → 2.1.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/README.md +9 -1
- package/dist/chat-agent-base.d.ts +253 -0
- package/dist/chat-agent-base.js +4 -0
- package/dist/chat-agent-base.js.map +1 -0
- package/dist/chat-messages.d.ts +481 -0
- package/dist/chat-messages.js +3 -0
- package/dist/chat-messages.js.map +1 -0
- package/dist/chunk-G5P5JXRF.js +1068 -0
- package/dist/chunk-G5P5JXRF.js.map +1 -0
- package/dist/chunk-OEX3D4WL.js +292 -0
- package/dist/chunk-OEX3D4WL.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +17 -12
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
import { safeParseClientMessage, isAssistantMessage } from './chunk-OEX3D4WL.js';
|
|
2
|
+
import { exhaustiveGuard } from '@firtoz/maybe-error';
|
|
3
|
+
import { OpenRouter } from '@openrouter/sdk';
|
|
4
|
+
import { Agent } from 'agents';
|
|
5
|
+
|
|
6
|
+
var DEFAULT_SYSTEM_PROMPT = "You are a helpful AI assistant.";
|
|
7
|
+
var CHUNK_BUFFER_SIZE = 10;
|
|
8
|
+
var CHUNK_BUFFER_MAX_SIZE = 100;
|
|
9
|
+
var STREAM_STALE_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
10
|
+
var CLEANUP_INTERVAL_MS = 10 * 60 * 1e3;
|
|
11
|
+
var CLEANUP_AGE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
12
|
+
var DEFAULT_MAX_TOOL_CONTENT_CHARS = 2e5;
|
|
13
|
+
var STREAM_TOOL_TOP_KEYS = /* @__PURE__ */ new Set(["index", "id", "type", "function"]);
|
|
14
|
+
function getRawToolCallDeltaEntry(chunk, index) {
|
|
15
|
+
const c = chunk;
|
|
16
|
+
const delta = c.choices?.[0]?.delta;
|
|
17
|
+
if (!delta) {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
const list = delta.tool_calls ?? delta.toolCalls;
|
|
21
|
+
if (!Array.isArray(list) || index < 0 || index >= list.length) {
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
const raw = list[index];
|
|
25
|
+
if (!raw || typeof raw !== "object") {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
function extractProviderMetadataFromRawToolPart(raw) {
|
|
31
|
+
const meta = {};
|
|
32
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
33
|
+
if (STREAM_TOOL_TOP_KEYS.has(k)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
meta[k] = v;
|
|
37
|
+
}
|
|
38
|
+
const fn = raw.function;
|
|
39
|
+
if (fn && typeof fn === "object" && !Array.isArray(fn)) {
|
|
40
|
+
const f = fn;
|
|
41
|
+
const fnExtra = {};
|
|
42
|
+
for (const [k, v] of Object.entries(f)) {
|
|
43
|
+
if (k === "name" || k === "arguments") {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
fnExtra[k] = v;
|
|
47
|
+
}
|
|
48
|
+
if (Object.keys(fnExtra).length > 0) {
|
|
49
|
+
meta.function = fnExtra;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
53
|
+
}
|
|
54
|
+
var ChatAgentBase = class extends Agent {
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Constructor - Following @cloudflare/ai-chat pattern
|
|
57
|
+
// ============================================================================
|
|
58
|
+
constructor(ctx, env) {
|
|
59
|
+
super(ctx, env);
|
|
60
|
+
/** In-memory cache of messages */
|
|
61
|
+
this.messages = [];
|
|
62
|
+
/** Map of message IDs to AbortControllers for request cancellation */
|
|
63
|
+
this._abortControllers = /* @__PURE__ */ new Map();
|
|
64
|
+
/** Currently active stream ID */
|
|
65
|
+
this._activeStreamId = null;
|
|
66
|
+
/** Message ID being streamed */
|
|
67
|
+
this._activeMessageId = null;
|
|
68
|
+
/** True only while the OpenRouter async iterator for the active stream is running */
|
|
69
|
+
this._openRouterStreamLive = false;
|
|
70
|
+
/** Current chunk index for active stream */
|
|
71
|
+
this._streamChunkIndex = 0;
|
|
72
|
+
/** Buffer for chunks pending write */
|
|
73
|
+
this._chunkBuffer = [];
|
|
74
|
+
/** Lock for flush operations */
|
|
75
|
+
this._isFlushingChunks = false;
|
|
76
|
+
/** Last cleanup timestamp */
|
|
77
|
+
this._lastCleanupTime = 0;
|
|
78
|
+
/** Client-registered tools (tools defined at runtime from frontend) */
|
|
79
|
+
this._clientTools = /* @__PURE__ */ new Map();
|
|
80
|
+
/** FIFO serialization of chat turns (user sends, tool continuations, etc.) */
|
|
81
|
+
this._turnTail = Promise.resolve();
|
|
82
|
+
/** Bumped by {@link resetTurnState} to ignore stale async work */
|
|
83
|
+
this._turnGeneration = 0;
|
|
84
|
+
/** Connection id that last queued an auto-continue after client tools (for multi-tab hints) */
|
|
85
|
+
this._continuationOriginConnectionId = null;
|
|
86
|
+
this._pendingClientToolAutoContinue = [];
|
|
87
|
+
this._clientToolAutoContinueFlushScheduled = false;
|
|
88
|
+
/** Human-in-the-loop: server tools awaiting `toolApprovalResponse` (not queued — avoids deadlock). */
|
|
89
|
+
this._pendingToolApprovals = /* @__PURE__ */ new Map();
|
|
90
|
+
this.dbInitialize();
|
|
91
|
+
this.messages = this.dbLoadMessages();
|
|
92
|
+
this._restoreActiveStream();
|
|
93
|
+
this._openRouterStreamLive = false;
|
|
94
|
+
const _onConnect = this.onConnect.bind(this);
|
|
95
|
+
this.onConnect = async (connection, connCtx) => {
|
|
96
|
+
if (this._activeStreamId && this._activeMessageId) {
|
|
97
|
+
this._notifyStreamResuming(connection);
|
|
98
|
+
}
|
|
99
|
+
return _onConnect(connection, connCtx);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Persistence hooks (override in subclasses)
|
|
104
|
+
// ============================================================================
|
|
105
|
+
/**
|
|
106
|
+
* Transform a message immediately before it is written to storage.
|
|
107
|
+
* Default: return the message unchanged.
|
|
108
|
+
*/
|
|
109
|
+
sanitizeMessageForPersistence(msg) {
|
|
110
|
+
return msg;
|
|
111
|
+
}
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Turn coordination (subclasses / host code)
|
|
114
|
+
// ============================================================================
|
|
115
|
+
/**
|
|
116
|
+
* Resolves after all queued turns have finished and no OpenRouter stream is active.
|
|
117
|
+
*/
|
|
118
|
+
waitUntilStable() {
|
|
119
|
+
return this._turnTail.then(async () => {
|
|
120
|
+
while (this._openRouterStreamLive) {
|
|
121
|
+
await new Promise((r) => queueMicrotask(r));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Abort in-flight generation, clear pending client tool auto-continue batch, and invalidate queued async work.
|
|
127
|
+
* Call from custom clear handlers; {@link clearHistory} path also resets state.
|
|
128
|
+
*/
|
|
129
|
+
resetTurnState() {
|
|
130
|
+
this._turnGeneration++;
|
|
131
|
+
this._pendingClientToolAutoContinue = [];
|
|
132
|
+
this._clientToolAutoContinueFlushScheduled = false;
|
|
133
|
+
this._continuationOriginConnectionId = null;
|
|
134
|
+
for (const p of this._pendingToolApprovals.values()) {
|
|
135
|
+
p.resolve(false);
|
|
136
|
+
}
|
|
137
|
+
this._pendingToolApprovals.clear();
|
|
138
|
+
for (const id of [...this._abortControllers.keys()]) {
|
|
139
|
+
this._cancelRequest(id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* True when the last assistant message still has tool calls without matching tool role replies.
|
|
144
|
+
*/
|
|
145
|
+
hasPendingInteraction() {
|
|
146
|
+
if (this._pendingToolApprovals.size > 0) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const last = this.messages[this.messages.length - 1];
|
|
150
|
+
if (!last || last.role !== "assistant") {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (!last.toolCalls?.length) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const pending = new Set(last.toolCalls.map((t) => t.id));
|
|
157
|
+
for (const m of this.messages) {
|
|
158
|
+
if (m.role === "tool" && pending.has(m.toolCallId)) {
|
|
159
|
+
pending.delete(m.toolCallId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return pending.size > 0;
|
|
163
|
+
}
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Message Persistence
|
|
166
|
+
// ============================================================================
|
|
167
|
+
_persistMessage(msg) {
|
|
168
|
+
const sanitized = this.sanitizeMessageForPersistence(msg);
|
|
169
|
+
const stored = this._maybeTruncateToolMessageContent(sanitized);
|
|
170
|
+
this.dbSaveMessage(stored);
|
|
171
|
+
const max = this.maxPersistedMessages;
|
|
172
|
+
if (typeof max === "number" && max > 0) {
|
|
173
|
+
this.dbTrimMessagesToMax(max);
|
|
174
|
+
this.messages = this.dbLoadMessages();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
_maybeTruncateToolMessageContent(msg) {
|
|
178
|
+
if (msg.role !== "tool") {
|
|
179
|
+
return msg;
|
|
180
|
+
}
|
|
181
|
+
const content = msg.content;
|
|
182
|
+
if (content.length <= DEFAULT_MAX_TOOL_CONTENT_CHARS) {
|
|
183
|
+
return msg;
|
|
184
|
+
}
|
|
185
|
+
const truncated = content.slice(0, DEFAULT_MAX_TOOL_CONTENT_CHARS) + `
|
|
186
|
+
\u2026 [truncated ${content.length - DEFAULT_MAX_TOOL_CONTENT_CHARS} chars for storage]`;
|
|
187
|
+
return { ...msg, content: truncated };
|
|
188
|
+
}
|
|
189
|
+
_clearMessages() {
|
|
190
|
+
this.resetTurnState();
|
|
191
|
+
this.dbClearAll();
|
|
192
|
+
this._activeStreamId = null;
|
|
193
|
+
this._activeMessageId = null;
|
|
194
|
+
this._streamChunkIndex = 0;
|
|
195
|
+
this._chunkBuffer = [];
|
|
196
|
+
this.messages = [];
|
|
197
|
+
}
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// Stream Restoration (following @cloudflare/ai-chat pattern)
|
|
200
|
+
// ============================================================================
|
|
201
|
+
/**
|
|
202
|
+
* Restore active stream state if the agent was restarted during streaming.
|
|
203
|
+
* Called during construction to recover any interrupted streams.
|
|
204
|
+
*/
|
|
205
|
+
_restoreActiveStream() {
|
|
206
|
+
const stream = this.dbFindActiveStream();
|
|
207
|
+
if (!stream) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const streamAge = Date.now() - stream.createdAt.getTime();
|
|
211
|
+
if (streamAge > STREAM_STALE_THRESHOLD_MS) {
|
|
212
|
+
this.dbDeleteStreamWithChunks(stream.id);
|
|
213
|
+
console.warn(
|
|
214
|
+
`[ChatAgent] Deleted stale stream ${stream.id} (age: ${Math.round(streamAge / 1e3)}s)`
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this._activeStreamId = stream.id;
|
|
219
|
+
this._activeMessageId = stream.messageId;
|
|
220
|
+
const maxIndex = this.dbFindMaxChunkIndex(stream.id);
|
|
221
|
+
this._streamChunkIndex = maxIndex != null ? maxIndex + 1 : 0;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Notify a connection about an active stream that can be resumed.
|
|
225
|
+
*/
|
|
226
|
+
_notifyStreamResuming(connection) {
|
|
227
|
+
if (!this._activeStreamId || !this._activeMessageId) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this._sendTo(connection, {
|
|
231
|
+
type: "streamResuming",
|
|
232
|
+
id: this._activeMessageId,
|
|
233
|
+
streamId: this._activeStreamId
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// Stream Chunk Management
|
|
238
|
+
// ============================================================================
|
|
239
|
+
_storeChunk(streamId, content) {
|
|
240
|
+
if (this._chunkBuffer.length >= CHUNK_BUFFER_MAX_SIZE) {
|
|
241
|
+
this._flushChunkBuffer();
|
|
242
|
+
}
|
|
243
|
+
this._chunkBuffer.push({
|
|
244
|
+
streamId,
|
|
245
|
+
content,
|
|
246
|
+
index: this._streamChunkIndex++
|
|
247
|
+
});
|
|
248
|
+
if (this._chunkBuffer.length >= CHUNK_BUFFER_SIZE) {
|
|
249
|
+
this._flushChunkBuffer();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
_flushChunkBuffer() {
|
|
253
|
+
if (this._isFlushingChunks || this._chunkBuffer.length === 0) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this._isFlushingChunks = true;
|
|
257
|
+
try {
|
|
258
|
+
const chunks = this._chunkBuffer;
|
|
259
|
+
this._chunkBuffer = [];
|
|
260
|
+
const chunksToInsert = chunks.map((chunk) => ({
|
|
261
|
+
id: crypto.randomUUID(),
|
|
262
|
+
streamId: chunk.streamId,
|
|
263
|
+
content: chunk.content,
|
|
264
|
+
chunkIndex: chunk.index
|
|
265
|
+
}));
|
|
266
|
+
this.dbInsertChunks(chunksToInsert);
|
|
267
|
+
} finally {
|
|
268
|
+
this._isFlushingChunks = false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
_startStream(messageId) {
|
|
272
|
+
this._flushChunkBuffer();
|
|
273
|
+
const streamId = crypto.randomUUID();
|
|
274
|
+
this._activeStreamId = streamId;
|
|
275
|
+
this._activeMessageId = messageId;
|
|
276
|
+
this._streamChunkIndex = 0;
|
|
277
|
+
this.dbInsertStreamMetadata(streamId, messageId);
|
|
278
|
+
return streamId;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Complete stream with a full message (supports tool calls)
|
|
282
|
+
*/
|
|
283
|
+
_completeStreamWithMessage(streamId, message) {
|
|
284
|
+
this._flushChunkBuffer();
|
|
285
|
+
this.dbUpdateStreamStatus(streamId, "completed");
|
|
286
|
+
this._persistMessage(message);
|
|
287
|
+
if (typeof this.maxPersistedMessages !== "number") {
|
|
288
|
+
this.messages.push(message);
|
|
289
|
+
}
|
|
290
|
+
this.dbDeleteChunks(streamId);
|
|
291
|
+
this._activeStreamId = null;
|
|
292
|
+
this._activeMessageId = null;
|
|
293
|
+
this._streamChunkIndex = 0;
|
|
294
|
+
this._maybeCleanupOldStreams();
|
|
295
|
+
}
|
|
296
|
+
_markStreamError(streamId) {
|
|
297
|
+
this._flushChunkBuffer();
|
|
298
|
+
this.dbUpdateStreamStatus(streamId, "error");
|
|
299
|
+
this._activeStreamId = null;
|
|
300
|
+
this._activeMessageId = null;
|
|
301
|
+
this._streamChunkIndex = 0;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Clean up old completed streams periodically.
|
|
305
|
+
*/
|
|
306
|
+
_maybeCleanupOldStreams() {
|
|
307
|
+
const now = Date.now();
|
|
308
|
+
if (now - this._lastCleanupTime < CLEANUP_INTERVAL_MS) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this._lastCleanupTime = now;
|
|
312
|
+
const cutoffMs = now - CLEANUP_AGE_THRESHOLD_MS;
|
|
313
|
+
this.dbDeleteOldCompletedStreams(cutoffMs);
|
|
314
|
+
}
|
|
315
|
+
_getStreamChunks(streamId) {
|
|
316
|
+
this._flushChunkBuffer();
|
|
317
|
+
return this.dbGetChunks(streamId);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Finalize a stream that has buffered chunks but no live OpenRouter reader (e.g. after DO restart).
|
|
321
|
+
*/
|
|
322
|
+
_finalizeOrphanedStreamFromChunks(streamId) {
|
|
323
|
+
if (this._activeStreamId !== streamId || !this._activeMessageId) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const messageId = this._activeMessageId;
|
|
327
|
+
const chunks = this._getStreamChunks(streamId);
|
|
328
|
+
const text = chunks.join("");
|
|
329
|
+
const assistantMessage = {
|
|
330
|
+
id: messageId,
|
|
331
|
+
role: "assistant",
|
|
332
|
+
content: text.length > 0 ? text : null,
|
|
333
|
+
createdAt: Date.now()
|
|
334
|
+
};
|
|
335
|
+
this._completeStreamWithMessage(streamId, assistantMessage);
|
|
336
|
+
this._broadcast({
|
|
337
|
+
type: "messageEnd",
|
|
338
|
+
id: messageId,
|
|
339
|
+
createdAt: assistantMessage.createdAt
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// Abort Controller Management
|
|
344
|
+
// ============================================================================
|
|
345
|
+
_getAbortSignal(id) {
|
|
346
|
+
let controller = this._abortControllers.get(id);
|
|
347
|
+
if (!controller) {
|
|
348
|
+
controller = new AbortController();
|
|
349
|
+
this._abortControllers.set(id, controller);
|
|
350
|
+
}
|
|
351
|
+
return controller.signal;
|
|
352
|
+
}
|
|
353
|
+
_cancelRequest(id) {
|
|
354
|
+
const controller = this._abortControllers.get(id);
|
|
355
|
+
if (controller) {
|
|
356
|
+
controller.abort();
|
|
357
|
+
this._abortControllers.delete(id);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
_removeAbortController(id) {
|
|
361
|
+
this._abortControllers.delete(id);
|
|
362
|
+
}
|
|
363
|
+
// ============================================================================
|
|
364
|
+
// Broadcasting
|
|
365
|
+
// ============================================================================
|
|
366
|
+
_broadcast(msg) {
|
|
367
|
+
this.broadcast(JSON.stringify(msg));
|
|
368
|
+
}
|
|
369
|
+
_sendTo(connection, msg) {
|
|
370
|
+
connection.send(JSON.stringify(msg));
|
|
371
|
+
}
|
|
372
|
+
_enqueueTurn(fn) {
|
|
373
|
+
const run = this._turnTail.then(fn);
|
|
374
|
+
this._turnTail = run.then(
|
|
375
|
+
() => {
|
|
376
|
+
},
|
|
377
|
+
() => {
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
return run;
|
|
381
|
+
}
|
|
382
|
+
_resolveToolApproval(approvalId, approved) {
|
|
383
|
+
const pending = this._pendingToolApprovals.get(approvalId);
|
|
384
|
+
if (!pending) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this._pendingToolApprovals.delete(approvalId);
|
|
388
|
+
pending.resolve(approved);
|
|
389
|
+
}
|
|
390
|
+
_mergeProviderMetadata(a, b) {
|
|
391
|
+
if (!a && !b) {
|
|
392
|
+
return void 0;
|
|
393
|
+
}
|
|
394
|
+
return { ...a ?? {}, ...b ?? {} };
|
|
395
|
+
}
|
|
396
|
+
_replaceMessagesFromClient(messages) {
|
|
397
|
+
this.resetTurnState();
|
|
398
|
+
this._flushChunkBuffer();
|
|
399
|
+
if (this._activeStreamId) {
|
|
400
|
+
this.dbDeleteStreamWithChunks(this._activeStreamId);
|
|
401
|
+
}
|
|
402
|
+
this._activeStreamId = null;
|
|
403
|
+
this._activeMessageId = null;
|
|
404
|
+
this._streamChunkIndex = 0;
|
|
405
|
+
this._chunkBuffer = [];
|
|
406
|
+
this.dbReplaceAllMessages([...messages]);
|
|
407
|
+
this.messages = [...messages];
|
|
408
|
+
}
|
|
409
|
+
// ============================================================================
|
|
410
|
+
// OpenRouter Integration
|
|
411
|
+
// ============================================================================
|
|
412
|
+
_getOpenRouter() {
|
|
413
|
+
const envWithGateway = this.env;
|
|
414
|
+
const serverURL = envWithGateway.CLOUDFLARE_ACCOUNT_ID && envWithGateway.AI_GATEWAY_NAME ? `https://gateway.ai.cloudflare.com/v1/${envWithGateway.CLOUDFLARE_ACCOUNT_ID}/${envWithGateway.AI_GATEWAY_NAME}/openrouter` : void 0;
|
|
415
|
+
return new OpenRouter({
|
|
416
|
+
apiKey: this.env.OPENROUTER_API_KEY,
|
|
417
|
+
...serverURL && { serverURL }
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// WebSocket Handlers
|
|
422
|
+
// ============================================================================
|
|
423
|
+
async onConnect(connection, _ctx) {
|
|
424
|
+
this._sendTo(connection, { type: "history", messages: this.messages });
|
|
425
|
+
}
|
|
426
|
+
async onClose(connection, _code, _reason, _wasClean) {
|
|
427
|
+
if (this._continuationOriginConnectionId === connection.id) {
|
|
428
|
+
this._continuationOriginConnectionId = null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async onMessage(connection, message) {
|
|
432
|
+
const data = safeParseClientMessage(message);
|
|
433
|
+
if (!data) {
|
|
434
|
+
console.error("Invalid client message:", message);
|
|
435
|
+
this._sendTo(connection, {
|
|
436
|
+
type: "error",
|
|
437
|
+
message: "Invalid message format"
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
switch (data.type) {
|
|
443
|
+
case "getHistory":
|
|
444
|
+
this._sendTo(connection, {
|
|
445
|
+
type: "history",
|
|
446
|
+
messages: this.messages
|
|
447
|
+
});
|
|
448
|
+
break;
|
|
449
|
+
case "clearHistory":
|
|
450
|
+
await this._enqueueTurn(async () => {
|
|
451
|
+
this._clearMessages();
|
|
452
|
+
this._broadcast({ type: "history", messages: [] });
|
|
453
|
+
});
|
|
454
|
+
break;
|
|
455
|
+
case "sendMessage":
|
|
456
|
+
await this._enqueueTurn(async () => {
|
|
457
|
+
this._continuationOriginConnectionId = connection.id;
|
|
458
|
+
await this._handleSendMessagePayload(data);
|
|
459
|
+
});
|
|
460
|
+
break;
|
|
461
|
+
case "toolApprovalResponse":
|
|
462
|
+
this._resolveToolApproval(data.approvalId, data.approved);
|
|
463
|
+
break;
|
|
464
|
+
case "resumeStream":
|
|
465
|
+
this._handleResumeStream(data.streamId);
|
|
466
|
+
break;
|
|
467
|
+
case "cancelRequest":
|
|
468
|
+
this._cancelRequest(data.id);
|
|
469
|
+
break;
|
|
470
|
+
case "toolResult":
|
|
471
|
+
await this._handleToolResultMessage(
|
|
472
|
+
connection,
|
|
473
|
+
data.toolCallId,
|
|
474
|
+
data.toolName,
|
|
475
|
+
data.output,
|
|
476
|
+
data.autoContinue ?? false
|
|
477
|
+
);
|
|
478
|
+
break;
|
|
479
|
+
case "registerTools":
|
|
480
|
+
await this._enqueueTurn(async () => {
|
|
481
|
+
this._registerClientTools(
|
|
482
|
+
connection,
|
|
483
|
+
data.tools
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
break;
|
|
487
|
+
default:
|
|
488
|
+
exhaustiveGuard(data);
|
|
489
|
+
}
|
|
490
|
+
} catch (err) {
|
|
491
|
+
console.error("Error processing message:", err);
|
|
492
|
+
this._broadcast({
|
|
493
|
+
type: "error",
|
|
494
|
+
message: "Failed to process message"
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// ============================================================================
|
|
499
|
+
// Chat Message Handling
|
|
500
|
+
// ============================================================================
|
|
501
|
+
/**
|
|
502
|
+
* Get the system prompt for the AI
|
|
503
|
+
* Override this method to customize the AI's behavior
|
|
504
|
+
*/
|
|
505
|
+
getSystemPrompt() {
|
|
506
|
+
return DEFAULT_SYSTEM_PROMPT;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Get the AI model to use
|
|
510
|
+
* Override this method to use a different model
|
|
511
|
+
*/
|
|
512
|
+
getModel() {
|
|
513
|
+
return "anthropic/claude-sonnet-4.5";
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Get available tools for the AI
|
|
517
|
+
* Override this method to provide custom tools
|
|
518
|
+
*/
|
|
519
|
+
getTools() {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
async _handleSendMessagePayload(data) {
|
|
523
|
+
const trigger = data.trigger ?? "submit-message";
|
|
524
|
+
if (trigger === "regenerate-message") {
|
|
525
|
+
this._replaceMessagesFromClient(data.messages ?? []);
|
|
526
|
+
await this._generateAIResponse();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (data.messages && data.messages.length > 0) {
|
|
530
|
+
this._replaceMessagesFromClient(data.messages);
|
|
531
|
+
}
|
|
532
|
+
const content = data.content;
|
|
533
|
+
if (content !== void 0 && content !== "") {
|
|
534
|
+
const userMessage = {
|
|
535
|
+
id: crypto.randomUUID(),
|
|
536
|
+
role: "user",
|
|
537
|
+
content,
|
|
538
|
+
createdAt: Date.now()
|
|
539
|
+
};
|
|
540
|
+
this._persistMessage(userMessage);
|
|
541
|
+
if (typeof this.maxPersistedMessages !== "number") {
|
|
542
|
+
this.messages.push(userMessage);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const last = this.messages[this.messages.length - 1];
|
|
546
|
+
if (!last || last.role !== "user") {
|
|
547
|
+
this._broadcast({
|
|
548
|
+
type: "error",
|
|
549
|
+
message: "Cannot generate: conversation must end with a user message (sync `messages` and/or send `content`)."
|
|
550
|
+
});
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
await this._generateAIResponse();
|
|
554
|
+
}
|
|
555
|
+
async _handleToolResultMessage(connection, toolCallId, toolName, output, autoContinue) {
|
|
556
|
+
if (!autoContinue) {
|
|
557
|
+
await this._enqueueTurn(async () => {
|
|
558
|
+
await this._applyClientToolResult(connection, toolCallId, output);
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
this._pendingClientToolAutoContinue.push({
|
|
563
|
+
connection,
|
|
564
|
+
toolCallId,
|
|
565
|
+
toolName,
|
|
566
|
+
output
|
|
567
|
+
});
|
|
568
|
+
this._scheduleClientToolAutoContinueFlush();
|
|
569
|
+
}
|
|
570
|
+
_scheduleClientToolAutoContinueFlush() {
|
|
571
|
+
if (this._clientToolAutoContinueFlushScheduled) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
this._clientToolAutoContinueFlushScheduled = true;
|
|
575
|
+
queueMicrotask(() => {
|
|
576
|
+
this._clientToolAutoContinueFlushScheduled = false;
|
|
577
|
+
const batch = this._pendingClientToolAutoContinue.splice(0);
|
|
578
|
+
if (batch.length === 0) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
void this._enqueueTurn(async () => {
|
|
582
|
+
const origin = batch[0]?.connection;
|
|
583
|
+
if (origin) {
|
|
584
|
+
this._continuationOriginConnectionId = origin.id;
|
|
585
|
+
}
|
|
586
|
+
for (const item of batch) {
|
|
587
|
+
await this._applyClientToolResult(
|
|
588
|
+
item.connection,
|
|
589
|
+
item.toolCallId,
|
|
590
|
+
item.output
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
await this._generateAIResponse();
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async _applyClientToolResult(_connection, toolCallId, output) {
|
|
598
|
+
const assistantMsg = this.messages.find(
|
|
599
|
+
(m) => isAssistantMessage(m) && m.toolCalls?.some((tc) => tc.id === toolCallId)
|
|
600
|
+
);
|
|
601
|
+
if (!assistantMsg) {
|
|
602
|
+
console.warn(
|
|
603
|
+
`[ChatAgent] Tool result for unknown tool call: ${toolCallId}`
|
|
604
|
+
);
|
|
605
|
+
this._broadcast({ type: "error", message: "Tool call not found" });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const toolMessage = {
|
|
609
|
+
id: crypto.randomUUID(),
|
|
610
|
+
role: "tool",
|
|
611
|
+
toolCallId,
|
|
612
|
+
content: JSON.stringify(output),
|
|
613
|
+
createdAt: Date.now()
|
|
614
|
+
};
|
|
615
|
+
this._persistMessage(toolMessage);
|
|
616
|
+
if (typeof this.maxPersistedMessages !== "number") {
|
|
617
|
+
this.messages.push(toolMessage);
|
|
618
|
+
}
|
|
619
|
+
this._broadcast({ type: "messageUpdated", message: toolMessage });
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Generate AI response (can be called for initial message or after tool results)
|
|
623
|
+
*/
|
|
624
|
+
async _generateAIResponse() {
|
|
625
|
+
const generation = this._turnGeneration;
|
|
626
|
+
const assistantId = crypto.randomUUID();
|
|
627
|
+
const streamId = this._startStream(assistantId);
|
|
628
|
+
const abortSignal = this._getAbortSignal(assistantId);
|
|
629
|
+
this._broadcast({ type: "messageStart", id: assistantId, streamId });
|
|
630
|
+
const runStream = async () => {
|
|
631
|
+
let fullContent = "";
|
|
632
|
+
let usage;
|
|
633
|
+
const toolCallsInProgress = /* @__PURE__ */ new Map();
|
|
634
|
+
const openRouter = this._getOpenRouter();
|
|
635
|
+
const toolsMap = this._getToolsMap();
|
|
636
|
+
const tools = Array.from(toolsMap.values());
|
|
637
|
+
const apiMessages = this._buildApiMessages();
|
|
638
|
+
const envWithGateway = this.env;
|
|
639
|
+
const headers = envWithGateway.AI_GATEWAY_TOKEN ? {
|
|
640
|
+
"cf-aig-authorization": `Bearer ${envWithGateway.AI_GATEWAY_TOKEN}`
|
|
641
|
+
} : void 0;
|
|
642
|
+
const stream = await openRouter.chat.send(
|
|
643
|
+
{
|
|
644
|
+
model: this.getModel(),
|
|
645
|
+
messages: apiMessages,
|
|
646
|
+
stream: true,
|
|
647
|
+
...tools.length > 0 && { tools }
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
...headers && { headers },
|
|
651
|
+
signal: abortSignal
|
|
652
|
+
}
|
|
653
|
+
);
|
|
654
|
+
this._openRouterStreamLive = true;
|
|
655
|
+
try {
|
|
656
|
+
for await (const chunk of stream) {
|
|
657
|
+
if (generation !== this._turnGeneration) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (abortSignal.aborted) {
|
|
661
|
+
throw new Error("Request cancelled");
|
|
662
|
+
}
|
|
663
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
664
|
+
if (delta?.content) {
|
|
665
|
+
fullContent += delta.content;
|
|
666
|
+
this._storeChunk(streamId, delta.content);
|
|
667
|
+
this._broadcast({
|
|
668
|
+
type: "messageChunk",
|
|
669
|
+
id: assistantId,
|
|
670
|
+
chunk: delta.content
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
if (delta?.toolCalls) {
|
|
674
|
+
for (const toolCallDelta of delta.toolCalls) {
|
|
675
|
+
const index = toolCallDelta.index;
|
|
676
|
+
if (!toolCallsInProgress.has(index)) {
|
|
677
|
+
toolCallsInProgress.set(index, {
|
|
678
|
+
id: toolCallDelta.id || "",
|
|
679
|
+
name: toolCallDelta.function?.name || "",
|
|
680
|
+
arguments: ""
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
const tcRow = toolCallsInProgress.get(index);
|
|
684
|
+
if (tcRow) {
|
|
685
|
+
if (toolCallDelta.id) {
|
|
686
|
+
tcRow.id = toolCallDelta.id;
|
|
687
|
+
}
|
|
688
|
+
if (toolCallDelta.function?.name) {
|
|
689
|
+
tcRow.name = toolCallDelta.function.name;
|
|
690
|
+
}
|
|
691
|
+
if (toolCallDelta.function?.arguments) {
|
|
692
|
+
tcRow.arguments += toolCallDelta.function.arguments;
|
|
693
|
+
}
|
|
694
|
+
const rawEntry = getRawToolCallDeltaEntry(chunk, index);
|
|
695
|
+
const extra = rawEntry ? extractProviderMetadataFromRawToolPart(rawEntry) : void 0;
|
|
696
|
+
if (extra) {
|
|
697
|
+
tcRow.providerMetadata = this._mergeProviderMetadata(
|
|
698
|
+
tcRow.providerMetadata,
|
|
699
|
+
extra
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const deltaMsg = {
|
|
704
|
+
index: toolCallDelta.index,
|
|
705
|
+
id: toolCallDelta.id,
|
|
706
|
+
type: toolCallDelta.type,
|
|
707
|
+
function: toolCallDelta.function ? {
|
|
708
|
+
name: toolCallDelta.function.name,
|
|
709
|
+
arguments: toolCallDelta.function.arguments
|
|
710
|
+
} : void 0,
|
|
711
|
+
...tcRow?.providerMetadata && Object.keys(tcRow.providerMetadata).length > 0 && {
|
|
712
|
+
providerMetadata: tcRow.providerMetadata
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
this._broadcast({
|
|
716
|
+
type: "toolCallDelta",
|
|
717
|
+
id: assistantId,
|
|
718
|
+
delta: deltaMsg
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (chunk.usage) {
|
|
723
|
+
usage = {
|
|
724
|
+
prompt_tokens: chunk.usage.promptTokens ?? 0,
|
|
725
|
+
completion_tokens: chunk.usage.completionTokens ?? 0,
|
|
726
|
+
total_tokens: chunk.usage.totalTokens ?? 0
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const finalToolCalls = [];
|
|
731
|
+
for (const [, tc] of toolCallsInProgress) {
|
|
732
|
+
if (tc.id && tc.name) {
|
|
733
|
+
const toolCall = {
|
|
734
|
+
id: tc.id,
|
|
735
|
+
type: "function",
|
|
736
|
+
function: {
|
|
737
|
+
name: tc.name,
|
|
738
|
+
arguments: tc.arguments
|
|
739
|
+
},
|
|
740
|
+
...tc.providerMetadata && Object.keys(tc.providerMetadata).length > 0 && {
|
|
741
|
+
providerMetadata: tc.providerMetadata
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
finalToolCalls.push(toolCall);
|
|
745
|
+
this._broadcast({
|
|
746
|
+
type: "toolCall",
|
|
747
|
+
id: assistantId,
|
|
748
|
+
toolCall
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const assistantMessage = {
|
|
753
|
+
id: assistantId,
|
|
754
|
+
role: "assistant",
|
|
755
|
+
content: fullContent || null,
|
|
756
|
+
toolCalls: finalToolCalls.length > 0 ? finalToolCalls : void 0,
|
|
757
|
+
createdAt: Date.now()
|
|
758
|
+
};
|
|
759
|
+
this._completeStreamWithMessage(streamId, assistantMessage);
|
|
760
|
+
this._removeAbortController(assistantId);
|
|
761
|
+
this._broadcast({
|
|
762
|
+
type: "messageEnd",
|
|
763
|
+
id: assistantId,
|
|
764
|
+
toolCalls: finalToolCalls.length > 0 ? finalToolCalls : void 0,
|
|
765
|
+
createdAt: assistantMessage.createdAt,
|
|
766
|
+
...usage && { usage }
|
|
767
|
+
});
|
|
768
|
+
if (finalToolCalls.length > 0) {
|
|
769
|
+
const hasServerTools = await this._executeServerSideTools(finalToolCalls);
|
|
770
|
+
if (hasServerTools) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
} catch (err) {
|
|
775
|
+
console.error("OpenRouter error:", err);
|
|
776
|
+
this._markStreamError(streamId);
|
|
777
|
+
this._removeAbortController(assistantId);
|
|
778
|
+
this._broadcast({
|
|
779
|
+
type: "error",
|
|
780
|
+
message: err instanceof Error ? err.message : "Failed to get AI response"
|
|
781
|
+
});
|
|
782
|
+
} finally {
|
|
783
|
+
this._openRouterStreamLive = false;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
await this.experimental_waitUntil(runStream);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Build API messages from our message history
|
|
790
|
+
*/
|
|
791
|
+
_buildApiMessages() {
|
|
792
|
+
const result = [
|
|
793
|
+
{ role: "system", content: this.getSystemPrompt() }
|
|
794
|
+
];
|
|
795
|
+
for (const msg of this.messages) {
|
|
796
|
+
switch (msg.role) {
|
|
797
|
+
case "user":
|
|
798
|
+
result.push({ role: "user", content: msg.content });
|
|
799
|
+
break;
|
|
800
|
+
case "assistant": {
|
|
801
|
+
const assistantMsg = msg;
|
|
802
|
+
const orMsg = {
|
|
803
|
+
role: "assistant",
|
|
804
|
+
content: assistantMsg.content,
|
|
805
|
+
...assistantMsg.toolCalls && {
|
|
806
|
+
toolCalls: assistantMsg.toolCalls.map((tc) => {
|
|
807
|
+
const call = {
|
|
808
|
+
id: tc.id,
|
|
809
|
+
type: "function",
|
|
810
|
+
function: {
|
|
811
|
+
name: tc.function.name,
|
|
812
|
+
arguments: tc.function.arguments
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
const meta = tc.providerMetadata;
|
|
816
|
+
if (meta) {
|
|
817
|
+
const {
|
|
818
|
+
id: _i,
|
|
819
|
+
type: _t,
|
|
820
|
+
function: _fn,
|
|
821
|
+
...rest
|
|
822
|
+
} = meta;
|
|
823
|
+
Object.assign(call, rest);
|
|
824
|
+
}
|
|
825
|
+
return call;
|
|
826
|
+
})
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
result.push(orMsg);
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
case "tool": {
|
|
833
|
+
const toolMsg = msg;
|
|
834
|
+
result.push({
|
|
835
|
+
role: "tool",
|
|
836
|
+
content: toolMsg.content,
|
|
837
|
+
toolCallId: toolMsg.toolCallId
|
|
838
|
+
});
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
default:
|
|
842
|
+
exhaustiveGuard(msg);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return result;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Build a map of tool definitions by name for quick lookup
|
|
849
|
+
*/
|
|
850
|
+
_getToolsMap() {
|
|
851
|
+
const tools = this.getTools();
|
|
852
|
+
const map = new Map(tools.map((t) => [t.function.name, t]));
|
|
853
|
+
for (const [name, tool] of this._clientTools) {
|
|
854
|
+
if (!map.has(name)) {
|
|
855
|
+
const toolDef = {
|
|
856
|
+
type: "function",
|
|
857
|
+
function: {
|
|
858
|
+
name: tool.name
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
if (tool.description !== void 0) {
|
|
862
|
+
toolDef.function.description = tool.description;
|
|
863
|
+
}
|
|
864
|
+
if (tool.parameters !== void 0) {
|
|
865
|
+
toolDef.function.parameters = tool.parameters;
|
|
866
|
+
}
|
|
867
|
+
map.set(name, toolDef);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return map;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Register tools from the client at runtime
|
|
874
|
+
*/
|
|
875
|
+
_registerClientTools(_connection, tools) {
|
|
876
|
+
for (const tool of tools) {
|
|
877
|
+
const entry = {
|
|
878
|
+
name: tool.name
|
|
879
|
+
};
|
|
880
|
+
if (tool.description !== void 0) {
|
|
881
|
+
entry.description = tool.description;
|
|
882
|
+
}
|
|
883
|
+
if (tool.parameters !== void 0) {
|
|
884
|
+
entry.parameters = tool.parameters;
|
|
885
|
+
}
|
|
886
|
+
this._clientTools.set(tool.name, entry);
|
|
887
|
+
console.log(`[ChatAgent] Registered client tool: ${tool.name}`);
|
|
888
|
+
}
|
|
889
|
+
this._broadcast({
|
|
890
|
+
type: "history",
|
|
891
|
+
messages: this.messages
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Execute server-side tools and continue the conversation
|
|
896
|
+
*/
|
|
897
|
+
async _executeServerSideTools(toolCalls) {
|
|
898
|
+
const toolsMap = this._getToolsMap();
|
|
899
|
+
let executedServerTools = false;
|
|
900
|
+
for (const toolCall of toolCalls) {
|
|
901
|
+
const toolDef = toolsMap.get(toolCall.function.name);
|
|
902
|
+
if (!toolDef) {
|
|
903
|
+
this._broadcast({
|
|
904
|
+
type: "toolError",
|
|
905
|
+
errorType: "not_found",
|
|
906
|
+
toolCallId: toolCall.id,
|
|
907
|
+
toolName: toolCall.function.name,
|
|
908
|
+
message: `Tool "${toolCall.function.name}" not found`
|
|
909
|
+
});
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (!toolDef.execute) {
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
executedServerTools = true;
|
|
916
|
+
try {
|
|
917
|
+
let args;
|
|
918
|
+
try {
|
|
919
|
+
args = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {};
|
|
920
|
+
} catch (parseErr) {
|
|
921
|
+
this._broadcast({
|
|
922
|
+
type: "toolError",
|
|
923
|
+
errorType: "input",
|
|
924
|
+
toolCallId: toolCall.id,
|
|
925
|
+
toolName: toolCall.function.name,
|
|
926
|
+
message: `Invalid JSON arguments: ${parseErr instanceof Error ? parseErr.message : "Parse error"}`
|
|
927
|
+
});
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (toolDef.needsApproval) {
|
|
931
|
+
const needApproval = await toolDef.needsApproval(args);
|
|
932
|
+
if (needApproval) {
|
|
933
|
+
const approvalId = crypto.randomUUID();
|
|
934
|
+
const approved = await new Promise((resolve) => {
|
|
935
|
+
this._pendingToolApprovals.set(approvalId, { resolve });
|
|
936
|
+
this._broadcast({
|
|
937
|
+
type: "toolApprovalRequest",
|
|
938
|
+
approvalId,
|
|
939
|
+
toolCallId: toolCall.id,
|
|
940
|
+
toolName: toolCall.function.name,
|
|
941
|
+
arguments: toolCall.function.arguments
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
if (!approved) {
|
|
945
|
+
const errorMsg = "Tool execution rejected by user";
|
|
946
|
+
this._broadcast({
|
|
947
|
+
type: "toolError",
|
|
948
|
+
errorType: "output",
|
|
949
|
+
toolCallId: toolCall.id,
|
|
950
|
+
toolName: toolCall.function.name,
|
|
951
|
+
message: errorMsg
|
|
952
|
+
});
|
|
953
|
+
const rejectedMessage = {
|
|
954
|
+
id: crypto.randomUUID(),
|
|
955
|
+
role: "tool",
|
|
956
|
+
toolCallId: toolCall.id,
|
|
957
|
+
content: JSON.stringify({
|
|
958
|
+
error: errorMsg,
|
|
959
|
+
rejected: true
|
|
960
|
+
}),
|
|
961
|
+
createdAt: Date.now()
|
|
962
|
+
};
|
|
963
|
+
this._persistMessage(rejectedMessage);
|
|
964
|
+
if (typeof this.maxPersistedMessages !== "number") {
|
|
965
|
+
this.messages.push(rejectedMessage);
|
|
966
|
+
}
|
|
967
|
+
this._broadcast({
|
|
968
|
+
type: "messageUpdated",
|
|
969
|
+
message: rejectedMessage
|
|
970
|
+
});
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
console.log(
|
|
976
|
+
`[ChatAgent] Executing server tool: ${toolCall.function.name}`,
|
|
977
|
+
args
|
|
978
|
+
);
|
|
979
|
+
const result = await toolDef.execute(args);
|
|
980
|
+
const toolMessage = {
|
|
981
|
+
id: crypto.randomUUID(),
|
|
982
|
+
role: "tool",
|
|
983
|
+
toolCallId: toolCall.id,
|
|
984
|
+
content: JSON.stringify(result),
|
|
985
|
+
createdAt: Date.now()
|
|
986
|
+
};
|
|
987
|
+
this._persistMessage(toolMessage);
|
|
988
|
+
if (typeof this.maxPersistedMessages !== "number") {
|
|
989
|
+
this.messages.push(toolMessage);
|
|
990
|
+
}
|
|
991
|
+
this._broadcast({ type: "messageUpdated", message: toolMessage });
|
|
992
|
+
console.log(
|
|
993
|
+
`[ChatAgent] Server tool completed: ${toolCall.function.name}`,
|
|
994
|
+
result
|
|
995
|
+
);
|
|
996
|
+
} catch (err) {
|
|
997
|
+
console.error(
|
|
998
|
+
`[ChatAgent] Server tool error: ${toolCall.function.name}`,
|
|
999
|
+
err
|
|
1000
|
+
);
|
|
1001
|
+
const errorMsg = err instanceof Error ? err.message : "Tool execution failed";
|
|
1002
|
+
this._broadcast({
|
|
1003
|
+
type: "toolError",
|
|
1004
|
+
errorType: "output",
|
|
1005
|
+
toolCallId: toolCall.id,
|
|
1006
|
+
toolName: toolCall.function.name,
|
|
1007
|
+
message: errorMsg
|
|
1008
|
+
});
|
|
1009
|
+
const errorMessage = {
|
|
1010
|
+
id: crypto.randomUUID(),
|
|
1011
|
+
role: "tool",
|
|
1012
|
+
toolCallId: toolCall.id,
|
|
1013
|
+
content: JSON.stringify({ error: errorMsg }),
|
|
1014
|
+
createdAt: Date.now()
|
|
1015
|
+
};
|
|
1016
|
+
this._persistMessage(errorMessage);
|
|
1017
|
+
if (typeof this.maxPersistedMessages !== "number") {
|
|
1018
|
+
this.messages.push(errorMessage);
|
|
1019
|
+
}
|
|
1020
|
+
this._broadcast({
|
|
1021
|
+
type: "messageUpdated",
|
|
1022
|
+
message: errorMessage
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (executedServerTools) {
|
|
1027
|
+
await this._generateAIResponse();
|
|
1028
|
+
}
|
|
1029
|
+
return executedServerTools;
|
|
1030
|
+
}
|
|
1031
|
+
_handleResumeStream(streamId) {
|
|
1032
|
+
if (!this.dbIsStreamKnown(streamId)) {
|
|
1033
|
+
this._broadcast({
|
|
1034
|
+
type: "streamResume",
|
|
1035
|
+
streamId,
|
|
1036
|
+
chunks: [],
|
|
1037
|
+
done: true
|
|
1038
|
+
});
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const chunks = this._getStreamChunks(streamId);
|
|
1042
|
+
const isLive = this._openRouterStreamLive && this._activeStreamId === streamId;
|
|
1043
|
+
this._broadcast({
|
|
1044
|
+
type: "streamResume",
|
|
1045
|
+
streamId,
|
|
1046
|
+
chunks,
|
|
1047
|
+
done: !isLive
|
|
1048
|
+
});
|
|
1049
|
+
if (!isLive && this._activeStreamId === streamId) {
|
|
1050
|
+
this._finalizeOrphanedStreamFromChunks(streamId);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
// ============================================================================
|
|
1054
|
+
// Cleanup
|
|
1055
|
+
// ============================================================================
|
|
1056
|
+
async destroy() {
|
|
1057
|
+
for (const controller of this._abortControllers.values()) {
|
|
1058
|
+
controller.abort();
|
|
1059
|
+
}
|
|
1060
|
+
this._abortControllers.clear();
|
|
1061
|
+
this._flushChunkBuffer();
|
|
1062
|
+
await super.destroy();
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
export { ChatAgentBase };
|
|
1067
|
+
//# sourceMappingURL=chunk-G5P5JXRF.js.map
|
|
1068
|
+
//# sourceMappingURL=chunk-G5P5JXRF.js.map
|