@jskit-ai/assistant-runtime 0.1.193 → 0.1.194
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/fixtures/canonical-conversation/App.vue +188 -0
- package/fixtures/canonical-conversation/DraftConversation.vue +32 -0
- package/fixtures/canonical-conversation/index.html +1 -0
- package/fixtures/canonical-conversation/main.js +61 -0
- package/fixtures/canonical-conversation/socket.js +50 -0
- package/fixtures/canonical-conversation/vite.config.mjs +178 -0
- package/fixtures/conversation/vite.config.mjs +12 -2
- package/package.json +23 -10
- package/src/client/components/AssistantConversationClientElement.vue +57 -0
- package/src/client/composables/useAssistantConversation.js +874 -0
- package/src/client/index.js +2 -0
- package/src/client/support/assistantRuntimeState.js +1 -1
- package/src/client/support/subscribeAssistantConversation.js +205 -0
- package/src/server/AssistantProvider.js +82 -106
- package/src/server/actionIds.js +10 -0
- package/src/server/actions.js +287 -79
- package/src/server/createAssistantRuntime.js +138 -0
- package/src/server/createAssistantSettingsActions.js +89 -0
- package/src/server/index.js +6 -0
- package/src/server/inputSchemas.js +90 -0
- package/src/server/registerConversationSubscriptions.js +163 -0
- package/src/server/registerFastifyConversations.js +76 -0
- package/src/server/registerRoutes.js +220 -198
- package/src/server/registerSettingsRoutes.js +113 -0
- package/src/server/repositories/conversationsRepository.js +12 -5
- package/src/server/repositories/messagesRepository.js +12 -0
- package/src/server/services/chatService.js +88 -694
- package/src/server/services/transcriptService.js +56 -52
- package/src/server/sqlConversationStorage.js +192 -0
- package/src/server/support/assistantRouteContext.js +91 -0
- package/src/server/support/createSurfaceAwareToolCatalog.js +6 -2
- package/src/shared/assistantSurfaces.js +14 -0
- package/src/shared/conversationRealtime.js +9 -0
- package/src/shared/index.js +1 -0
- package/test/actionDefinitions.test.js +44 -0
- package/test/aiConnections.test.js +1 -1
- package/test/assistantConversationBinding.browser.test.js +1868 -0
- package/test/assistantConversationOperations.test.js +459 -0
- package/test/assistantConversationSubscriptions.test.js +692 -0
- package/test/assistantIntegrationBoundary.test.js +103 -0
- package/test/assistantRuntimeComposition.test.js +68 -0
- package/test/assistantSurfacePattern.test.js +1 -1
- package/test/assistantSurfaces.test.js +23 -0
- package/test/chatServiceLifecycle.test.js +12 -1
- package/test/featureRuntime.test.js +238 -0
- package/test/registerFastifyConversations.test.js +169 -0
- package/test/sqlConversationStorage.test.js +205 -0
- package/test/support/memoryAssistantRows.js +51 -0
- package/test/turnRequests.test.js +11 -2
- package/src/shared/assistantResponseText.js +0 -29
|
@@ -2,20 +2,12 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
3
3
|
import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
4
|
import { resolveWorkspaceSlug } from "@jskit-ai/assistant-core/server";
|
|
5
|
-
import {
|
|
6
|
-
ASSISTANT_STREAM_EVENT_TYPES
|
|
7
|
-
} from "@jskit-ai/assistant-core/shared";
|
|
5
|
+
import { ASSISTANT_STREAM_EVENT_TYPES, isAssistantProgressOnlyText } from "@jskit-ai/assistant-core/shared";
|
|
8
6
|
import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
|
|
9
|
-
import { isAssistantProgressOnlyText } from "../../shared/assistantResponseText.js";
|
|
10
7
|
|
|
11
8
|
const MAX_HISTORY_MESSAGES = 20;
|
|
12
9
|
const MAX_INPUT_CHARS = 8000;
|
|
13
|
-
const MAX_TOOL_ROUNDS = 16;
|
|
14
|
-
const MAX_RECOVERY_PASSES = 3;
|
|
15
|
-
const MAX_TOOL_RESULT_FALLBACK_CHARS = 4000;
|
|
16
|
-
const CURRENT_TIME_PREFLIGHT_INTENT = "current-time";
|
|
17
10
|
const CLOCK_INSTRUCTION = "For current or relative date and time questions, first use any available authoritative workspace clock action; never infer the current date or time from model knowledge.";
|
|
18
|
-
const COMPLETION_INSTRUCTION = "Do not narrate future work or describe what you are about to do. Either call the required available tool now or provide the completed final answer.";
|
|
19
11
|
|
|
20
12
|
function normalizeConversationId(value) {
|
|
21
13
|
return normalizeRecordId(value, { fallback: null });
|
|
@@ -99,58 +91,6 @@ function isAbortError(error) {
|
|
|
99
91
|
return String(error.name || "").trim() === "AbortError";
|
|
100
92
|
}
|
|
101
93
|
|
|
102
|
-
function requiresCurrentTime(value = "") {
|
|
103
|
-
const text = normalizeText(value);
|
|
104
|
-
if (!text) {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return [
|
|
109
|
-
/\b(?:now|today|tomorrow|yesterday|tonight)\b/iu,
|
|
110
|
-
/\b(?:current|local)\s+(?:date|day|time|date\s+and\s+time)\b/iu,
|
|
111
|
-
/\bwhat(?:'s|\s+is)\s+(?:the\s+)?(?:date|day|time)\b/iu,
|
|
112
|
-
/\b(?:this|next|last)\s+(?:day|week|month|year|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/iu
|
|
113
|
-
].some((pattern) => pattern.test(text));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function resolvePreflightTools(toolDescriptors = [], input = "") {
|
|
117
|
-
if (!requiresCurrentTime(input)) {
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const currentTimeTool = toolDescriptors.find((tool) => {
|
|
122
|
-
const intents = Array.isArray(tool?.preflight) ? tool.preflight : [];
|
|
123
|
-
const requiredParameters = Array.isArray(tool?.parameters?.required)
|
|
124
|
-
? tool.parameters.required
|
|
125
|
-
: [];
|
|
126
|
-
return requiredParameters.length < 1 && intents.includes(CURRENT_TIME_PREFLIGHT_INTENT);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
return currentTimeTool ? [currentTimeTool] : [];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function extractTextDelta(deltaContent) {
|
|
133
|
-
if (typeof deltaContent === "string") {
|
|
134
|
-
return deltaContent;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (!Array.isArray(deltaContent)) {
|
|
138
|
-
return "";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return deltaContent
|
|
142
|
-
.map((entry) => {
|
|
143
|
-
if (typeof entry === "string") {
|
|
144
|
-
return entry;
|
|
145
|
-
}
|
|
146
|
-
if (!entry || typeof entry !== "object") {
|
|
147
|
-
return "";
|
|
148
|
-
}
|
|
149
|
-
return String(entry.text || "");
|
|
150
|
-
})
|
|
151
|
-
.join("");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
94
|
function toCompactJson(value, fallback = "{}") {
|
|
155
95
|
try {
|
|
156
96
|
if (!value || typeof value !== "object") {
|
|
@@ -217,235 +157,6 @@ function buildSystemPrompt({ targetSurfaceId = "", toolDescriptors = [], workspa
|
|
|
217
157
|
return promptSegments.join(" ");
|
|
218
158
|
}
|
|
219
159
|
|
|
220
|
-
function buildRecoveryPrompt({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
|
|
221
|
-
const normalizedReason = normalizeText(reason).toLowerCase();
|
|
222
|
-
const failureSummary = (Array.isArray(toolFailures) ? toolFailures : [])
|
|
223
|
-
.slice(0, 3)
|
|
224
|
-
.map((entry) => {
|
|
225
|
-
const toolName = normalizeText(entry?.name) || "unknown_tool";
|
|
226
|
-
const errorCode = normalizeText(entry?.error?.code) || "tool_failed";
|
|
227
|
-
return `${toolName}:${errorCode}`;
|
|
228
|
-
})
|
|
229
|
-
.filter(Boolean)
|
|
230
|
-
.join(", ");
|
|
231
|
-
const successSummary = (Array.isArray(toolSuccesses) ? toolSuccesses : [])
|
|
232
|
-
.slice(0, 3)
|
|
233
|
-
.map((entry) => normalizeText(entry?.name))
|
|
234
|
-
.filter(Boolean)
|
|
235
|
-
.join(", ");
|
|
236
|
-
|
|
237
|
-
const failureSuffix = failureSummary ? ` Recent tool failures: ${failureSummary}.` : "";
|
|
238
|
-
const successSuffix = successSummary ? ` Successful tools: ${successSummary}.` : "";
|
|
239
|
-
if (normalizedReason === "tool_failure") {
|
|
240
|
-
return `One or more tool calls may fail. Continue with available successful results. Do not output function-call markup. Do not mention failed operations unless explicitly asked. ${COMPLETION_INSTRUCTION}${failureSuffix}${successSuffix}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return `Tool-call rounds were exhausted. Provide the best direct answer with available context and successful results only. ${COMPLETION_INSTRUCTION}${failureSuffix}${successSuffix}`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function buildRecoveryFallbackAnswer({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
|
|
247
|
-
if (normalizeText(reason).toLowerCase() === "max_tool_rounds") {
|
|
248
|
-
return "Limit reached. Start a new conversation.";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return buildToolOutcomeFallbackAnswer({
|
|
252
|
-
toolFailures,
|
|
253
|
-
toolSuccesses
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function toSafeToolResultText(value) {
|
|
258
|
-
if (value == null) {
|
|
259
|
-
return "null";
|
|
260
|
-
}
|
|
261
|
-
if (typeof value === "string") {
|
|
262
|
-
const normalized = normalizeText(value);
|
|
263
|
-
return normalized || "\"\"";
|
|
264
|
-
}
|
|
265
|
-
try {
|
|
266
|
-
return JSON.stringify(value, null, 2);
|
|
267
|
-
} catch {
|
|
268
|
-
return "\"<unserializable>\"";
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function buildToolOutcomeFallbackAnswer({ toolFailures = [], toolSuccesses = [] } = {}) {
|
|
273
|
-
const successfulResults = (Array.isArray(toolSuccesses) ? toolSuccesses : [])
|
|
274
|
-
.filter((entry) => normalizeText(entry?.name));
|
|
275
|
-
const hasFailures = Array.isArray(toolFailures) && toolFailures.length > 0;
|
|
276
|
-
|
|
277
|
-
if (successfulResults.length > 0) {
|
|
278
|
-
const latestSuccess = successfulResults.at(-1);
|
|
279
|
-
const answer = `Latest successful result from ${normalizeText(latestSuccess.name)}:\n${toSafeToolResultText(latestSuccess.result)}`;
|
|
280
|
-
if (answer.length <= MAX_TOOL_RESULT_FALLBACK_CHARS) {
|
|
281
|
-
return answer;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const suffix = "\n…[truncated]";
|
|
285
|
-
return `${answer.slice(0, MAX_TOOL_RESULT_FALLBACK_CHARS - suffix.length)}${suffix}`;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (hasFailures) {
|
|
289
|
-
return "I could not gather additional information from successful operations.";
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return "I could not gather additional information from the available operations.";
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function sanitizeAssistantMessageText(value) {
|
|
296
|
-
let source = String(value || "");
|
|
297
|
-
if (!source) {
|
|
298
|
-
return "";
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const blockPatterns = [
|
|
302
|
-
/<[^>\n]*function_calls[^>\n]*>[\s\S]*?(?:<\/[^>\n]*function_calls>|$)/gi,
|
|
303
|
-
/<[^>\n]*tool_calls?[^>\n]*>[\s\S]*?(?:<\/[^>\n]*tool_calls?[^>\n]*>|$)/gi,
|
|
304
|
-
/<[^>\n]*invoke\b[^>\n]*>[\s\S]*?(?:<\/[^>\n]*invoke>|$)/gi,
|
|
305
|
-
/<(?:analysis|reasoning|think)>[\s\S]*?(?:<\/(?:analysis|reasoning|think)>|$)/gi
|
|
306
|
-
];
|
|
307
|
-
for (const pattern of blockPatterns) {
|
|
308
|
-
source = source.replace(pattern, " ");
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const inlineTagPatterns = [
|
|
312
|
-
/<[^>\n]*invoke\b[^>]*\/>/gi,
|
|
313
|
-
/<\/?[^>\n]*invoke[^>\n]*>/gi,
|
|
314
|
-
/<\/?[^>\n]*function_calls[^>\n]*>/gi,
|
|
315
|
-
/<\/?[^>\n]*tool_calls?[^>\n]*>/gi,
|
|
316
|
-
/<\/?[^>\n]*DSML[^>\n]*>/gi
|
|
317
|
-
];
|
|
318
|
-
for (const pattern of inlineTagPatterns) {
|
|
319
|
-
source = source.replace(pattern, " ");
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return source
|
|
323
|
-
.split(/\r?\n/)
|
|
324
|
-
.map((line) => line.trim())
|
|
325
|
-
.filter(Boolean)
|
|
326
|
-
.join("\n");
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function buildAssistantToolCallMessage(toolCalls = []) {
|
|
330
|
-
return {
|
|
331
|
-
role: "assistant",
|
|
332
|
-
content: "",
|
|
333
|
-
tool_calls: toolCalls.map((toolCall) => ({
|
|
334
|
-
id: toolCall.id,
|
|
335
|
-
type: "function",
|
|
336
|
-
function: {
|
|
337
|
-
name: toolCall.name,
|
|
338
|
-
arguments: toolCall.arguments
|
|
339
|
-
}
|
|
340
|
-
}))
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function parseDsmlToolCallsFromText(value = "") {
|
|
345
|
-
const source = String(value || "");
|
|
346
|
-
if (!source) {
|
|
347
|
-
return [];
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const functionCallsMatch = source.match(
|
|
351
|
-
/<[^>\n]*function_calls[^>\n]*>([\s\S]*?)<\/[^>\n]*function_calls>/i
|
|
352
|
-
);
|
|
353
|
-
if (!functionCallsMatch) {
|
|
354
|
-
return [];
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const blockText = String(functionCallsMatch[1] || "");
|
|
358
|
-
if (!blockText) {
|
|
359
|
-
return [];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const calls = [];
|
|
363
|
-
const invokePattern = /<[^>\n]*invoke\b([^>]*)>([\s\S]*?)<\/[^>\n]*invoke>/gi;
|
|
364
|
-
let match = invokePattern.exec(blockText);
|
|
365
|
-
while (match) {
|
|
366
|
-
const attributes = String(match[1] || "");
|
|
367
|
-
const body = normalizeText(String(match[2] || ""));
|
|
368
|
-
const quotedNameMatch =
|
|
369
|
-
attributes.match(/\bname\s*=\s*"([^"]+)"/i) || attributes.match(/\bname\s*=\s*'([^']+)'/i);
|
|
370
|
-
const bareNameMatch = attributes.match(/\bname\s*=\s*([^\s"'/>]+)/i);
|
|
371
|
-
const name = normalizeText(quotedNameMatch?.[1] || bareNameMatch?.[1]);
|
|
372
|
-
if (name) {
|
|
373
|
-
calls.push({
|
|
374
|
-
id: `dsml_tool_call_${calls.length + 1}`,
|
|
375
|
-
name,
|
|
376
|
-
arguments: body && /^[\[{]/.test(body) ? body : "{}"
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
match = invokePattern.exec(blockText);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return calls;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async function consumeCompletionStream(stream, onText = () => {}) {
|
|
387
|
-
let assistantText = "";
|
|
388
|
-
const toolCallsByIndex = new Map();
|
|
389
|
-
|
|
390
|
-
for await (const chunk of stream) {
|
|
391
|
-
const choice = chunk?.choices?.[0] || {};
|
|
392
|
-
const delta = choice?.delta || {};
|
|
393
|
-
|
|
394
|
-
const textDelta = extractTextDelta(delta.content);
|
|
395
|
-
if (textDelta) {
|
|
396
|
-
assistantText += textDelta;
|
|
397
|
-
// An opening provider tag can be split between chunks. Keep it private
|
|
398
|
-
// until its boundary is known; complete internal blocks are stripped below.
|
|
399
|
-
const tagStart = assistantText.lastIndexOf("<");
|
|
400
|
-
const safeText = tagStart > assistantText.lastIndexOf(">") ? assistantText.slice(0, tagStart) : assistantText;
|
|
401
|
-
onText(sanitizeAssistantMessageText(safeText));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
405
|
-
for (const partialToolCall of toolCalls) {
|
|
406
|
-
const index = Number(partialToolCall?.index || 0);
|
|
407
|
-
const existing =
|
|
408
|
-
toolCallsByIndex.get(index) ||
|
|
409
|
-
{
|
|
410
|
-
id: normalizeText(partialToolCall?.id) || `tool_call_${index + 1}`,
|
|
411
|
-
name: "",
|
|
412
|
-
arguments: ""
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
if (partialToolCall?.id) {
|
|
416
|
-
existing.id = normalizeText(partialToolCall.id) || existing.id;
|
|
417
|
-
}
|
|
418
|
-
if (partialToolCall?.function?.name) {
|
|
419
|
-
existing.name += String(partialToolCall.function.name || "");
|
|
420
|
-
}
|
|
421
|
-
if (partialToolCall?.function?.arguments) {
|
|
422
|
-
existing.arguments += String(partialToolCall.function.arguments || "");
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
toolCallsByIndex.set(index, existing);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
let toolCalls = [...toolCallsByIndex.values()].map((toolCall, index) => ({
|
|
430
|
-
id: normalizeText(toolCall.id) || `tool_call_${index + 1}`,
|
|
431
|
-
name: normalizeText(toolCall.name),
|
|
432
|
-
arguments: String(toolCall.arguments || "")
|
|
433
|
-
}));
|
|
434
|
-
|
|
435
|
-
if (toolCalls.length < 1) {
|
|
436
|
-
const parsedDsmlCalls = parseDsmlToolCallsFromText(assistantText);
|
|
437
|
-
if (parsedDsmlCalls.length > 0) {
|
|
438
|
-
toolCalls = parsedDsmlCalls;
|
|
439
|
-
assistantText = normalizeText(sanitizeAssistantMessageText(assistantText));
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
assistantText,
|
|
445
|
-
toolCalls
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
|
|
449
160
|
function requireAssistantSurface(appConfig = {}, targetSurfaceId = "") {
|
|
450
161
|
const assistantSurface = resolveAssistantSurfaceConfig(appConfig, targetSurfaceId);
|
|
451
162
|
if (assistantSurface) {
|
|
@@ -463,6 +174,7 @@ function buildAssistantActionContext(context = {}, assistantSurface = {}) {
|
|
|
463
174
|
}
|
|
464
175
|
|
|
465
176
|
function createChatService({
|
|
177
|
+
conversationRuntime,
|
|
466
178
|
aiClientFactory,
|
|
467
179
|
turnRequests,
|
|
468
180
|
attachments,
|
|
@@ -565,6 +277,7 @@ function createChatService({
|
|
|
565
277
|
return result;
|
|
566
278
|
|
|
567
279
|
async function executeTurn() {
|
|
280
|
+
if (typeof conversationRuntime?.open !== "function") throw new TypeError("Assistant streaming requires its common conversation runtime.");
|
|
568
281
|
const conversationResult = await transcriptService.createConversationForTurn(
|
|
569
282
|
assistantSurface,
|
|
570
283
|
workspace,
|
|
@@ -587,8 +300,9 @@ function createChatService({
|
|
|
587
300
|
throw new AppError(500, "Assistant failed to create conversation.");
|
|
588
301
|
}
|
|
589
302
|
|
|
590
|
-
|
|
591
|
-
|
|
303
|
+
const resolvedAttachments = [];
|
|
304
|
+
async function prepareAttachments(attachmentIds) {
|
|
305
|
+
if (attachmentIds === undefined || (Array.isArray(attachmentIds) && !attachmentIds.length)) return;
|
|
592
306
|
if (!Array.isArray(attachmentIds) || attachmentIds.length > 10 ||
|
|
593
307
|
attachmentIds.some(id => typeof id !== "string" || !id || id.length > 256)) {
|
|
594
308
|
throw new AppError(400, "Invalid attachment identifiers.");
|
|
@@ -596,422 +310,101 @@ function createChatService({
|
|
|
596
310
|
if (!attachments?.resolve || !aiClient.supportsAttachments) {
|
|
597
311
|
throw new AppError(400, "File attachments are not configured for this assistant.");
|
|
598
312
|
}
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const files = await resolveFiles(source.attachmentIds);
|
|
604
|
-
const history = [];
|
|
605
|
-
for (const message of source.history) {
|
|
606
|
-
const previousFiles = await resolveFiles(message.attachmentIds);
|
|
607
|
-
history.push({ role: message.role, content: previousFiles.content.length
|
|
608
|
-
? [{ type: "text", text: message.content }, ...previousFiles.content] : message.content });
|
|
313
|
+
// Preserve the facade's existing pre-admission authorization order.
|
|
314
|
+
// Only these server-resolved materials reach the common file reader.
|
|
315
|
+
const files = await attachments.resolve({ attachmentIds, context: assistantContext, workspace, conversation });
|
|
316
|
+
resolvedAttachments.push({ attachmentIds: [...attachmentIds], files });
|
|
609
317
|
}
|
|
610
|
-
|
|
611
|
-
await
|
|
612
|
-
assistantSurface,
|
|
613
|
-
conversationId,
|
|
614
|
-
{
|
|
615
|
-
role: "user",
|
|
616
|
-
kind: "chat",
|
|
617
|
-
clientMessageSid: source.messageId,
|
|
618
|
-
contentText: source.input,
|
|
619
|
-
metadata: {
|
|
620
|
-
surfaceId: assistantSurface.targetSurfaceId,
|
|
621
|
-
...(files.attachments.length ? { attachments: files.attachments } : {})
|
|
622
|
-
}
|
|
623
|
-
},
|
|
624
|
-
{
|
|
625
|
-
context: assistantContext,
|
|
626
|
-
workspace
|
|
627
|
-
}
|
|
628
|
-
);
|
|
629
|
-
|
|
318
|
+
await prepareAttachments(source.attachmentIds);
|
|
319
|
+
for (const message of source.history) await prepareAttachments(message.attachmentIds);
|
|
630
320
|
const toolSet = serviceToolCatalog.resolveToolSet(assistantContext);
|
|
631
321
|
const customSystemPrompt = await assistantConfigService.resolveSystemPrompt(
|
|
632
|
-
assistantSurface,
|
|
633
|
-
|
|
634
|
-
{
|
|
635
|
-
surface: assistantSurface.targetSurfaceId
|
|
636
|
-
},
|
|
637
|
-
{
|
|
638
|
-
context: assistantContext,
|
|
639
|
-
input: payload
|
|
640
|
-
}
|
|
322
|
+
assistantSurface, workspace, { surface: assistantSurface.targetSurfaceId },
|
|
323
|
+
{ context: assistantContext, input: payload }
|
|
641
324
|
);
|
|
642
|
-
const
|
|
643
|
-
targetSurfaceId: assistantSurface.targetSurfaceId,
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
{
|
|
656
|
-
role: "user",
|
|
657
|
-
content: files.content.length ? [{ type: "text", text: source.input }, ...files.content] : source.input
|
|
658
|
-
}
|
|
659
|
-
];
|
|
660
|
-
|
|
661
|
-
async function completeWithAssistantMessage(assistantMessageText, { metadata = {} } = {}) {
|
|
662
|
-
const normalizedAssistantMessageText = normalizeText(sanitizeAssistantMessageText(assistantMessageText));
|
|
663
|
-
if (!normalizedAssistantMessageText) {
|
|
664
|
-
throw new AppError(502, "Assistant returned no output.");
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
await transcriptService.appendMessage(
|
|
668
|
-
assistantSurface,
|
|
669
|
-
conversationId,
|
|
670
|
-
{
|
|
671
|
-
role: "assistant",
|
|
672
|
-
kind: "chat",
|
|
673
|
-
contentText: normalizedAssistantMessageText
|
|
674
|
-
},
|
|
675
|
-
{
|
|
676
|
-
context: assistantContext,
|
|
677
|
-
workspace
|
|
678
|
-
}
|
|
679
|
-
);
|
|
680
|
-
|
|
681
|
-
await transcriptService.completeConversation(
|
|
682
|
-
assistantSurface,
|
|
683
|
-
conversationId,
|
|
684
|
-
{
|
|
685
|
-
status: "completed",
|
|
686
|
-
metadata
|
|
687
|
-
},
|
|
688
|
-
{
|
|
689
|
-
context: assistantContext,
|
|
690
|
-
workspace
|
|
691
|
-
}
|
|
692
|
-
);
|
|
693
|
-
|
|
694
|
-
streamWriter.sendAssistantMessage({
|
|
695
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE,
|
|
696
|
-
text: normalizedAssistantMessageText
|
|
697
|
-
});
|
|
698
|
-
streamWriter.sendDone({
|
|
699
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
|
|
700
|
-
messageId: source.messageId,
|
|
701
|
-
status: "completed"
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
return {
|
|
705
|
-
conversationId,
|
|
706
|
-
messageId: source.messageId,
|
|
707
|
-
status: "completed"
|
|
708
|
-
};
|
|
325
|
+
const configuration = { systemPrompt: buildSystemPrompt({
|
|
326
|
+
targetSurfaceId: assistantSurface.targetSurfaceId, toolDescriptors: toolSet.tools,
|
|
327
|
+
workspaceSlug: resolveWorkspaceSlug(assistantContext, payload), customSystemPrompt
|
|
328
|
+
}), ...(request.integrationId ? { integrationId: request.integrationId } : {}) };
|
|
329
|
+
const runtimeContext = { ...assistantContext, assistantRequest: {
|
|
330
|
+
conversation, workspace, history: source.history, resolvedAttachments
|
|
331
|
+
} };
|
|
332
|
+
let handle;
|
|
333
|
+
try { handle = await conversationRuntime.open({ id: conversationId, context: runtimeContext, configuration }); }
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (error.code !== "conversation_configuration_mismatch") throw error;
|
|
336
|
+
handle = await conversationRuntime.open({ id: conversationId, context: runtimeContext });
|
|
337
|
+
await handle.configure(configuration);
|
|
709
338
|
}
|
|
710
|
-
|
|
339
|
+
let turnId = null;
|
|
340
|
+
let displayedId = null;
|
|
711
341
|
let displayedText = "";
|
|
712
|
-
|
|
713
|
-
if (
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
streamWriter.sendAssistantDelta({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA, delta: text.slice(displayedText.length) });
|
|
721
|
-
} else {
|
|
722
|
-
streamWriter.sendAssistantMessage({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE, text, status: "streaming" });
|
|
342
|
+
const release = await handle.subscribe(event => {
|
|
343
|
+
if (event.type === "accepted" && event.messageId === source.messageId) turnId = event.turnId;
|
|
344
|
+
if (!turnId || event.turnId !== turnId) return;
|
|
345
|
+
if (event.type === "message" && ["assistant", "commentary"].includes(event.role)) {
|
|
346
|
+
if (displayedId !== event.messageId) {
|
|
347
|
+
if (displayedText) streamWriter.sendAssistantMessage({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE, text: "", status: "streaming" });
|
|
348
|
+
displayedId = event.messageId;
|
|
349
|
+
displayedText = "";
|
|
723
350
|
}
|
|
724
|
-
displayedText
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
streamWriter.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
name: toolCall.name,
|
|
736
|
-
arguments: toolCall.arguments
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
await transcriptService.appendMessage(
|
|
740
|
-
assistantSurface,
|
|
741
|
-
conversationId,
|
|
742
|
-
{
|
|
743
|
-
role: "assistant",
|
|
744
|
-
kind: "tool_call",
|
|
745
|
-
contentText: toolCall.arguments,
|
|
746
|
-
metadata: {
|
|
747
|
-
toolCallId: toolCall.id,
|
|
748
|
-
tool: toolCall.name
|
|
749
|
-
}
|
|
750
|
-
},
|
|
751
|
-
{
|
|
752
|
-
context: assistantContext,
|
|
753
|
-
workspace
|
|
754
|
-
}
|
|
755
|
-
);
|
|
756
|
-
|
|
757
|
-
const toolResult = await serviceToolCatalog.executeToolCall({
|
|
758
|
-
toolName: toolCall.name,
|
|
759
|
-
argumentsText: toolCall.arguments,
|
|
760
|
-
context: assistantContext,
|
|
761
|
-
toolSet
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
await transcriptService.appendMessage(
|
|
765
|
-
assistantSurface,
|
|
766
|
-
conversationId,
|
|
767
|
-
{
|
|
768
|
-
role: "assistant",
|
|
769
|
-
kind: "tool_result",
|
|
770
|
-
contentText: JSON.stringify(toolResult),
|
|
771
|
-
metadata: {
|
|
772
|
-
toolCallId: toolCall.id,
|
|
773
|
-
tool: toolCall.name,
|
|
774
|
-
ok: toolResult.ok === true
|
|
775
|
-
}
|
|
776
|
-
},
|
|
777
|
-
{
|
|
778
|
-
context: assistantContext,
|
|
779
|
-
workspace
|
|
780
|
-
}
|
|
781
|
-
);
|
|
782
|
-
|
|
783
|
-
if (toolResult.ok) {
|
|
784
|
-
toolSuccesses.push({
|
|
785
|
-
name: toolCall.name,
|
|
786
|
-
result: toolResult.result
|
|
787
|
-
});
|
|
788
|
-
streamWriter.sendToolResult({
|
|
789
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
|
|
790
|
-
toolCallId: toolCall.id,
|
|
791
|
-
name: toolCall.name,
|
|
792
|
-
ok: true,
|
|
793
|
-
result: toolResult.result
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
messages.push({
|
|
797
|
-
role: "tool",
|
|
798
|
-
tool_call_id: toolCall.id,
|
|
799
|
-
content: JSON.stringify(toolResult.result ?? null)
|
|
800
|
-
});
|
|
801
|
-
continue;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const failure = {
|
|
805
|
-
name: toolCall.name,
|
|
806
|
-
error: toolResult.error
|
|
807
|
-
};
|
|
808
|
-
roundFailures.push(failure);
|
|
809
|
-
toolFailures.push(failure);
|
|
810
|
-
|
|
811
|
-
streamWriter.sendToolResult({
|
|
812
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
|
|
813
|
-
toolCallId: toolCall.id,
|
|
814
|
-
name: toolCall.name,
|
|
815
|
-
ok: false,
|
|
816
|
-
error: toolResult.error
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
messages.push({
|
|
820
|
-
role: "tool",
|
|
821
|
-
tool_call_id: toolCall.id,
|
|
822
|
-
content: JSON.stringify({
|
|
823
|
-
error: toolResult.error
|
|
824
|
-
})
|
|
825
|
-
});
|
|
351
|
+
if (event.text === displayedText) return;
|
|
352
|
+
if (event.text.startsWith(displayedText)) streamWriter.sendAssistantDelta({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA, delta: event.text.slice(displayedText.length) });
|
|
353
|
+
else streamWriter.sendAssistantMessage({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE, text: event.text, status: "streaming" });
|
|
354
|
+
displayedText = event.text;
|
|
355
|
+
} else if (event.type === "tool") {
|
|
356
|
+
const call = event.call;
|
|
357
|
+
if (call.status === "running") streamWriter.sendToolCall({ type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_CALL,
|
|
358
|
+
toolCallId: call.id, name: call.name, arguments: call.arguments });
|
|
359
|
+
else if (call.result) streamWriter.sendToolResult({ type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
|
|
360
|
+
toolCallId: call.id, name: call.name, ok: call.result.ok === true,
|
|
361
|
+
...(call.result.ok ? { result: call.result.result } : { error: call.result.error }) });
|
|
826
362
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
async function recoverWithoutTools({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
|
|
832
|
-
for (let pass = 0; pass < MAX_RECOVERY_PASSES; pass += 1) {
|
|
833
|
-
const recoveryMessages = [
|
|
834
|
-
...messages,
|
|
835
|
-
{
|
|
836
|
-
role: "system",
|
|
837
|
-
content: buildRecoveryPrompt({
|
|
838
|
-
reason,
|
|
839
|
-
toolFailures,
|
|
840
|
-
toolSuccesses
|
|
841
|
-
})
|
|
842
|
-
}
|
|
843
|
-
];
|
|
844
|
-
|
|
845
|
-
const completionStream = await aiClient.createChatCompletionStream({
|
|
846
|
-
messages: recoveryMessages,
|
|
847
|
-
tools: [],
|
|
848
|
-
signal: options.abortSignal
|
|
849
|
-
});
|
|
850
|
-
const completion = await consumeResponse(completionStream);
|
|
851
|
-
|
|
852
|
-
const recoveryToolCalls = completion.toolCalls.filter((entry) => entry.name);
|
|
853
|
-
if (recoveryToolCalls.length > 0) {
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const assistantMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
|
|
858
|
-
if (assistantMessageText && !isAssistantProgressOnlyText(assistantMessageText)) {
|
|
859
|
-
return completeWithAssistantMessage(assistantMessageText, {
|
|
860
|
-
metadata: {
|
|
861
|
-
recoveryReason: reason || "unknown",
|
|
862
|
-
toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const fallbackText = buildRecoveryFallbackAnswer({
|
|
869
|
-
reason,
|
|
870
|
-
toolFailures,
|
|
871
|
-
toolSuccesses
|
|
872
|
-
});
|
|
873
|
-
return completeWithAssistantMessage(fallbackText, {
|
|
874
|
-
metadata: {
|
|
875
|
-
recoveryReason: reason || "unknown",
|
|
876
|
-
toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
|
|
877
|
-
}
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
|
|
363
|
+
});
|
|
364
|
+
let cancellation;
|
|
365
|
+
const cancel = () => { cancellation ||= handle.cancel(); cancellation.catch(() => {}); };
|
|
881
366
|
let streamed = false;
|
|
882
|
-
|
|
883
367
|
try {
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
provider: aiClient.provider,
|
|
889
|
-
model: aiClient.defaultModel
|
|
890
|
-
};
|
|
891
|
-
await turnRequests.update(claim, { meta: receipt });
|
|
892
|
-
streamWriter.sendMeta(receipt);
|
|
368
|
+
const meta = { type: ASSISTANT_STREAM_EVENT_TYPES.META, messageId: source.messageId, conversationId,
|
|
369
|
+
provider: aiClient.provider, model: aiClient.defaultModel };
|
|
370
|
+
await turnRequests.update(claim, { meta });
|
|
371
|
+
streamWriter.sendMeta(meta);
|
|
893
372
|
streamed = true;
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
messages.push(buildAssistantToolCallMessage([toolCall]));
|
|
907
|
-
const preflightFailures = await executeToolCalls([toolCall], {
|
|
908
|
-
toolFailures,
|
|
909
|
-
toolSuccesses
|
|
910
|
-
});
|
|
911
|
-
for (const failure of preflightFailures) {
|
|
912
|
-
const toolName = normalizeText(failure?.name);
|
|
913
|
-
if (toolName) {
|
|
914
|
-
excludedToolNames.add(toolName);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
920
|
-
const roundToolDescriptors = toolSet.tools.filter(
|
|
921
|
-
(tool) => !excludedToolNames.has(normalizeText(tool.name))
|
|
922
|
-
);
|
|
923
|
-
const roundToolSchemas = roundToolDescriptors.map((tool) => serviceToolCatalog.toOpenAiToolSchema(tool));
|
|
924
|
-
|
|
925
|
-
const completionStream = await aiClient.createChatCompletionStream({
|
|
926
|
-
messages,
|
|
927
|
-
tools: roundToolSchemas,
|
|
928
|
-
signal: options.abortSignal
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
const completion = await consumeResponse(completionStream);
|
|
932
|
-
|
|
933
|
-
const toolCalls = completion.toolCalls.filter((entry) => entry.name);
|
|
934
|
-
if (toolCalls.length < 1) {
|
|
935
|
-
const finalMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
|
|
936
|
-
if (finalMessageText && !isAssistantProgressOnlyText(finalMessageText)) {
|
|
937
|
-
return completeWithAssistantMessage(finalMessageText, {
|
|
938
|
-
metadata: toolFailures.length > 0
|
|
939
|
-
? {
|
|
940
|
-
recoveryReason: "tool_failure",
|
|
941
|
-
toolFailureCount: toolFailures.length
|
|
942
|
-
}
|
|
943
|
-
: {}
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
messages.push({
|
|
948
|
-
role: "system",
|
|
949
|
-
content: COMPLETION_INSTRUCTION
|
|
950
|
-
});
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
messages.push(buildAssistantToolCallMessage(toolCalls));
|
|
955
|
-
|
|
956
|
-
const roundFailures = await executeToolCalls(toolCalls, {
|
|
957
|
-
toolFailures,
|
|
958
|
-
toolSuccesses
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
if (roundFailures.length > 0) {
|
|
962
|
-
for (const failure of roundFailures) {
|
|
963
|
-
const toolName = normalizeText(failure?.name);
|
|
964
|
-
if (toolName) {
|
|
965
|
-
excludedToolNames.add(toolName);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
}
|
|
373
|
+
options.abortSignal?.addEventListener("abort", cancel, { once: true });
|
|
374
|
+
options.abortSignal?.throwIfAborted();
|
|
375
|
+
const receipt = await handle.send({ messageId: source.messageId, text: source.input,
|
|
376
|
+
...(source.attachmentIds !== undefined ? { attachmentIds: source.attachmentIds } : {}) });
|
|
377
|
+
turnId = receipt.turnId;
|
|
378
|
+
const snapshot = await handle.wait();
|
|
379
|
+
const turn = snapshot.conversationLog.find(entry => entry.turnId === turnId);
|
|
380
|
+
const status = turn?.metadata?.runtime?.status;
|
|
381
|
+
if (status !== "complete") {
|
|
382
|
+
const error = new Error(turn?.metadata?.runtime?.error || snapshot.error || "Assistant request failed.");
|
|
383
|
+
if (status === "cancelled") error.name = "AbortError";
|
|
384
|
+
throw error;
|
|
969
385
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
});
|
|
386
|
+
const text = normalizeText(turn.assistant?.text);
|
|
387
|
+
if (!text) throw new AppError(502, "Assistant returned no output.");
|
|
388
|
+
await transcriptService.completeConversation(assistantSurface, conversationId,
|
|
389
|
+
{ status: "completed", metadata: turn.metadata?.completion || {} }, { context: assistantContext, workspace });
|
|
390
|
+
streamWriter.sendAssistantMessage({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE, text });
|
|
391
|
+
streamWriter.sendDone({ type: ASSISTANT_STREAM_EVENT_TYPES.DONE, messageId: source.messageId, status: "completed" });
|
|
392
|
+
return { conversationId, messageId: source.messageId, status: "completed" };
|
|
976
393
|
} catch (error) {
|
|
977
|
-
const aborted = isAbortError(error);
|
|
394
|
+
const aborted = isAbortError(error) || options.abortSignal?.aborted;
|
|
978
395
|
const status = aborted ? "aborted" : "failed";
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
);
|
|
992
|
-
|
|
993
|
-
streamWriter.sendError({
|
|
994
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.ERROR,
|
|
995
|
-
messageId: source.messageId,
|
|
996
|
-
code: aborted ? "assistant_stream_aborted" : String(error?.code || "assistant_stream_failed"),
|
|
997
|
-
message: aborted ? "Assistant request was cancelled." : String(error?.message || "Assistant request failed."),
|
|
998
|
-
status: aborted ? 499 : Number(error?.status || error?.statusCode || 500)
|
|
999
|
-
});
|
|
1000
|
-
|
|
1001
|
-
streamWriter.sendDone({
|
|
1002
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
|
|
1003
|
-
messageId: source.messageId,
|
|
1004
|
-
status
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
return {
|
|
1008
|
-
conversationId,
|
|
1009
|
-
messageId: source.messageId,
|
|
1010
|
-
status
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
throw error;
|
|
396
|
+
if (!streamed) throw error;
|
|
397
|
+
await transcriptService.completeConversation(assistantSurface, conversationId, { status }, { context: assistantContext, workspace });
|
|
398
|
+
streamWriter.sendError({ type: ASSISTANT_STREAM_EVENT_TYPES.ERROR, messageId: source.messageId,
|
|
399
|
+
code: aborted ? "assistant_stream_aborted" : String(error?.code || "assistant_stream_failed"),
|
|
400
|
+
message: aborted ? "Assistant request was cancelled." : String(error?.message || "Assistant request failed."),
|
|
401
|
+
status: aborted ? 499 : Number(error?.status || error?.statusCode || 500) });
|
|
402
|
+
streamWriter.sendDone({ type: ASSISTANT_STREAM_EVENT_TYPES.DONE, messageId: source.messageId, status });
|
|
403
|
+
return { conversationId, messageId: source.messageId, status };
|
|
404
|
+
} finally {
|
|
405
|
+
options.abortSignal?.removeEventListener("abort", cancel);
|
|
406
|
+
release();
|
|
407
|
+
await cancellation;
|
|
1015
408
|
}
|
|
1016
409
|
}
|
|
1017
410
|
}
|
|
@@ -1044,6 +437,7 @@ function createChatService({
|
|
|
1044
437
|
}
|
|
1045
438
|
|
|
1046
439
|
return Object.freeze({
|
|
440
|
+
close: () => conversationRuntime?.close(),
|
|
1047
441
|
streamChat,
|
|
1048
442
|
listConversations,
|
|
1049
443
|
getConversationMessages
|