@jskit-ai/assistant 0.1.38 → 0.1.39

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.
Files changed (29) hide show
  1. package/package.descriptor.mjs +84 -319
  2. package/package.json +2 -2
  3. package/src/server/buildTemplateContext.js +39 -14
  4. package/templates/src/pages/assistant/index.vue +2 -2
  5. package/templates/src/pages/settings/assistant/index.vue +7 -0
  6. package/test/buildTemplateContext.test.js +42 -27
  7. package/test/packageDescriptor.test.js +26 -55
  8. package/test/templateContracts.test.js +8 -6
  9. package/templates/migrations/assistant_config_initial.cjs +0 -25
  10. package/templates/migrations/assistant_transcripts_initial.cjs +0 -56
  11. package/templates/src/local-package/client/components/AssistantSettingsClientElement.vue +0 -88
  12. package/templates/src/local-package/client/components/AssistantSurfaceClientElement.vue +0 -10
  13. package/templates/src/local-package/client/composables/useAssistantRuntime.js +0 -754
  14. package/templates/src/local-package/client/index.js +0 -4
  15. package/templates/src/local-package/client/providers/AssistantClientProvider.js +0 -16
  16. package/templates/src/local-package/package.descriptor.mjs +0 -85
  17. package/templates/src/local-package/package.json +0 -11
  18. package/templates/src/local-package/server/AssistantProvider.js +0 -143
  19. package/templates/src/local-package/server/actionIds.js +0 -9
  20. package/templates/src/local-package/server/actions.js +0 -183
  21. package/templates/src/local-package/server/registerRoutes.js +0 -296
  22. package/templates/src/local-package/server/repositories/assistantConfigRepository.js +0 -141
  23. package/templates/src/local-package/server/repositories/conversationsRepository.js +0 -240
  24. package/templates/src/local-package/server/repositories/messagesRepository.js +0 -166
  25. package/templates/src/local-package/server/services/assistantConfigService.js +0 -90
  26. package/templates/src/local-package/server/services/chatService.js +0 -995
  27. package/templates/src/local-package/server/services/transcriptService.js +0 -308
  28. package/templates/src/local-package/shared/assistantRuntimeConfig.js +0 -13
  29. package/templates/src/local-package/shared/index.js +0 -1
@@ -1,995 +0,0 @@
1
- import { AppError, parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
2
- import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
- import { resolveWorkspace } from "@jskit-ai/users-core/server/support/resolveWorkspace";
4
- import { resolveWorkspaceSlug } from "@jskit-ai/assistant-core/server";
5
- import {
6
- ASSISTANT_STREAM_EVENT_TYPES
7
- } from "@jskit-ai/assistant-core/shared";
8
- import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
9
-
10
- const MAX_HISTORY_MESSAGES = 20;
11
- const MAX_INPUT_CHARS = 8000;
12
- const MAX_TOOL_ROUNDS = 4;
13
-
14
- function normalizeConversationId(value) {
15
- const parsed = parsePositiveInteger(value);
16
- return parsed > 0 ? parsed : null;
17
- }
18
-
19
- function normalizeHistory(history = []) {
20
- const source = Array.isArray(history) ? history : [];
21
- return source
22
- .slice(0, MAX_HISTORY_MESSAGES)
23
- .map((entry) => {
24
- const item = normalizeObject(entry);
25
- const role = normalizeText(item.role).toLowerCase();
26
- if (role !== "user" && role !== "assistant") {
27
- return null;
28
- }
29
-
30
- const content = normalizeText(item.content).slice(0, MAX_INPUT_CHARS);
31
- if (!content) {
32
- return null;
33
- }
34
-
35
- return {
36
- role,
37
- content
38
- };
39
- })
40
- .filter(Boolean);
41
- }
42
-
43
- function normalizeStreamInput(payload = {}) {
44
- const source = normalizeObject(payload);
45
- const messageId = normalizeText(source.messageId);
46
- const input = normalizeText(source.input).slice(0, MAX_INPUT_CHARS);
47
- if (!messageId) {
48
- throw new AppError(400, "Validation failed.", {
49
- details: {
50
- fieldErrors: {
51
- messageId: "messageId is required."
52
- }
53
- }
54
- });
55
- }
56
- if (!input) {
57
- throw new AppError(400, "Validation failed.", {
58
- details: {
59
- fieldErrors: {
60
- input: "input is required."
61
- }
62
- }
63
- });
64
- }
65
-
66
- return {
67
- messageId,
68
- conversationId: normalizeConversationId(source.conversationId),
69
- input,
70
- history: normalizeHistory(source.history)
71
- };
72
- }
73
-
74
- function hasStreamWriter(streamWriter) {
75
- return Boolean(
76
- streamWriter &&
77
- typeof streamWriter.sendMeta === "function" &&
78
- typeof streamWriter.sendAssistantDelta === "function" &&
79
- typeof streamWriter.sendAssistantMessage === "function" &&
80
- typeof streamWriter.sendToolCall === "function" &&
81
- typeof streamWriter.sendToolResult === "function" &&
82
- typeof streamWriter.sendError === "function" &&
83
- typeof streamWriter.sendDone === "function"
84
- );
85
- }
86
-
87
- function isAbortError(error) {
88
- if (!error) {
89
- return false;
90
- }
91
-
92
- return String(error.name || "").trim() === "AbortError";
93
- }
94
-
95
- function extractTextDelta(deltaContent) {
96
- if (typeof deltaContent === "string") {
97
- return deltaContent;
98
- }
99
-
100
- if (!Array.isArray(deltaContent)) {
101
- return "";
102
- }
103
-
104
- return deltaContent
105
- .map((entry) => {
106
- if (typeof entry === "string") {
107
- return entry;
108
- }
109
- if (!entry || typeof entry !== "object") {
110
- return "";
111
- }
112
- return String(entry.text || "");
113
- })
114
- .join("");
115
- }
116
-
117
- function toCompactJson(value, fallback = "{}") {
118
- try {
119
- if (!value || typeof value !== "object") {
120
- return fallback;
121
- }
122
- return JSON.stringify(value);
123
- } catch {
124
- return fallback;
125
- }
126
- }
127
-
128
- function buildToolContractLine(toolDescriptor = {}) {
129
- const name = normalizeText(toolDescriptor.name);
130
- if (!name) {
131
- return "";
132
- }
133
-
134
- const inputSchema = toolDescriptor.parameters;
135
- const outputSchema = toolDescriptor.outputSchema;
136
- return `${name}: input=${toCompactJson(inputSchema)} output=${toCompactJson(outputSchema, "null")}`;
137
- }
138
-
139
- function buildSystemPrompt({ toolDescriptors = [], workspaceSlug = "", customSystemPrompt = "" } = {}) {
140
- const toolSummary = toolDescriptors.length > 0
141
- ? `Available tools: ${toolDescriptors.map((entry) => entry.name).join(", ")}.`
142
- : "No tools are currently available for this user/session.";
143
- const toolContracts = toolDescriptors.length > 0
144
- ? `Tool contracts: ${toolDescriptors.map((entry) => buildToolContractLine(entry)).filter(Boolean).join(" | ")}.`
145
- : "Tool contracts: none.";
146
- const normalizedWorkspaceSlug = normalizeText(workspaceSlug).toLowerCase();
147
- const workspaceLine = normalizedWorkspaceSlug
148
- ? `Current workspace slug: ${normalizedWorkspaceSlug}.`
149
- : "No workspace context is active for this assistant.";
150
- const normalizedCustomSystemPrompt = String(customSystemPrompt || "").trim();
151
-
152
- const promptSegments = [
153
- `You are the assistant for the ${assistantRuntimeConfig.runtimeSurfaceId} surface.`,
154
- "Use tools when they are necessary and only when available.",
155
- "Do not mention tools that are not available.",
156
- "When answering schema questions, rely only on tool contracts and tool results.",
157
- workspaceLine,
158
- toolSummary,
159
- toolContracts
160
- ];
161
- if (normalizedCustomSystemPrompt) {
162
- promptSegments.push(`Additional instructions for this surface: ${normalizedCustomSystemPrompt}`);
163
- }
164
-
165
- return promptSegments.join(" ");
166
- }
167
-
168
- function buildRecoveryPrompt({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
169
- const normalizedReason = normalizeText(reason).toLowerCase();
170
- const failureSummary = (Array.isArray(toolFailures) ? toolFailures : [])
171
- .slice(0, 3)
172
- .map((entry) => {
173
- const toolName = normalizeText(entry?.name) || "unknown_tool";
174
- const errorCode = normalizeText(entry?.error?.code) || "tool_failed";
175
- return `${toolName}:${errorCode}`;
176
- })
177
- .filter(Boolean)
178
- .join(", ");
179
- const successSummary = (Array.isArray(toolSuccesses) ? toolSuccesses : [])
180
- .slice(0, 3)
181
- .map((entry) => normalizeText(entry?.name))
182
- .filter(Boolean)
183
- .join(", ");
184
-
185
- const failureSuffix = failureSummary ? ` Recent tool failures: ${failureSummary}.` : "";
186
- const successSuffix = successSummary ? ` Successful tools: ${successSummary}.` : "";
187
- if (normalizedReason === "tool_failure") {
188
- 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.${failureSuffix}${successSuffix}`;
189
- }
190
-
191
- return `Tool-call rounds were exhausted. Provide the best direct answer with available context and successful results only.${failureSuffix}${successSuffix}`;
192
- }
193
-
194
- function buildRecoveryFallbackAnswer({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
195
- const normalizedReason = normalizeText(reason).toLowerCase();
196
- if (normalizedReason === "tool_failure") {
197
- return buildToolOutcomeFallbackAnswer({
198
- toolFailures,
199
- toolSuccesses
200
- });
201
- }
202
-
203
- return "I reached the tool-call limit for this request. Please narrow the request and I will continue.";
204
- }
205
-
206
- function toSafeToolResultText(value) {
207
- if (value == null) {
208
- return "null";
209
- }
210
- if (typeof value === "string") {
211
- const normalized = normalizeText(value);
212
- return normalized || "\"\"";
213
- }
214
- try {
215
- return JSON.stringify(value, null, 2);
216
- } catch {
217
- return "\"<unserializable>\"";
218
- }
219
- }
220
-
221
- function buildToolOutcomeFallbackAnswer({ toolFailures = [], toolSuccesses = [] } = {}) {
222
- const successNames = [...new Set(
223
- (Array.isArray(toolSuccesses) ? toolSuccesses : [])
224
- .map((entry) => normalizeText(entry?.name))
225
- .filter(Boolean)
226
- )];
227
- const hasFailures = Array.isArray(toolFailures) && toolFailures.length > 0;
228
-
229
- if (successNames.length > 0) {
230
- const summaryLines = (Array.isArray(toolSuccesses) ? toolSuccesses : [])
231
- .filter((entry) => normalizeText(entry?.name))
232
- .map((entry) => {
233
- const name = normalizeText(entry.name);
234
- const payload = toSafeToolResultText(entry.result);
235
- return `- ${name}:\n\`\`\`json\n${payload}\n\`\`\``;
236
- });
237
-
238
- return [
239
- "I used the available successful results:",
240
- ...summaryLines
241
- ].join("\n");
242
- }
243
-
244
- if (hasFailures) {
245
- return "I could not gather additional information from successful operations.";
246
- }
247
-
248
- return "I could not gather additional information from the available operations.";
249
- }
250
-
251
- function sanitizeAssistantMessageText(value) {
252
- let source = String(value || "");
253
- if (!source) {
254
- return "";
255
- }
256
-
257
- const blockPatterns = [
258
- /<[^>\n]*function_calls[^>\n]*>[\s\S]*?<\/[^>\n]*function_calls>/gi,
259
- /<[^>\n]*tool_calls?[^>\n]*>[\s\S]*?<\/[^>\n]*tool_calls?[^>\n]*>/gi,
260
- /<[^>\n]*invoke\b[^>\n]*>[\s\S]*?<\/[^>\n]*invoke>/gi
261
- ];
262
- for (const pattern of blockPatterns) {
263
- source = source.replace(pattern, " ");
264
- }
265
-
266
- const inlineTagPatterns = [
267
- /<[^>\n]*invoke\b[^>]*\/>/gi,
268
- /<\/?[^>\n]*invoke[^>\n]*>/gi,
269
- /<\/?[^>\n]*function_calls[^>\n]*>/gi,
270
- /<\/?[^>\n]*tool_calls?[^>\n]*>/gi,
271
- /<\/?[^>\n]*DSML[^>\n]*>/gi
272
- ];
273
- for (const pattern of inlineTagPatterns) {
274
- source = source.replace(pattern, " ");
275
- }
276
-
277
- return source
278
- .split(/\r?\n/)
279
- .map((line) => line.trim())
280
- .filter(Boolean)
281
- .join("\n");
282
- }
283
-
284
- function buildAssistantToolCallMessage({ assistantText = "", toolCalls = [] } = {}) {
285
- return {
286
- role: "assistant",
287
- content: assistantText || "",
288
- tool_calls: toolCalls.map((toolCall) => ({
289
- id: toolCall.id,
290
- type: "function",
291
- function: {
292
- name: toolCall.name,
293
- arguments: toolCall.arguments
294
- }
295
- }))
296
- };
297
- }
298
-
299
- function parseDsmlToolCallsFromText(value = "") {
300
- const source = String(value || "");
301
- if (!source) {
302
- return [];
303
- }
304
-
305
- const functionCallsMatch = source.match(
306
- /<[^>\n]*function_calls[^>\n]*>([\s\S]*?)<\/[^>\n]*function_calls>/i
307
- );
308
- if (!functionCallsMatch) {
309
- return [];
310
- }
311
-
312
- const blockText = String(functionCallsMatch[1] || "");
313
- if (!blockText) {
314
- return [];
315
- }
316
-
317
- const calls = [];
318
- const invokePattern = /<[^>\n]*invoke\b([^>]*)>([\s\S]*?)<\/[^>\n]*invoke>/gi;
319
- let match = invokePattern.exec(blockText);
320
- while (match) {
321
- const attributes = String(match[1] || "");
322
- const body = normalizeText(String(match[2] || ""));
323
- const quotedNameMatch =
324
- attributes.match(/\bname\s*=\s*"([^"]+)"/i) || attributes.match(/\bname\s*=\s*'([^']+)'/i);
325
- const bareNameMatch = attributes.match(/\bname\s*=\s*([^\s"'/>]+)/i);
326
- const name = normalizeText(quotedNameMatch?.[1] || bareNameMatch?.[1]);
327
- if (name) {
328
- calls.push({
329
- id: `dsml_tool_call_${calls.length + 1}`,
330
- name,
331
- arguments: body && /^[\[{]/.test(body) ? body : "{}"
332
- });
333
- }
334
-
335
- match = invokePattern.exec(blockText);
336
- }
337
-
338
- return calls;
339
- }
340
-
341
- function createDsmlDeltaSanitizer() {
342
- let inTag = false;
343
- let tagBuffer = "";
344
- let suppressedDepth = 0;
345
-
346
- function resolveTagType(rawTag = "") {
347
- const normalizedTag = String(rawTag || "").toLowerCase();
348
- if (normalizedTag.includes("function_calls")) {
349
- return "function_calls";
350
- }
351
- if (normalizedTag.includes("tool_calls")) {
352
- return "tool_calls";
353
- }
354
- if (normalizedTag.includes("invoke")) {
355
- return "invoke";
356
- }
357
- return "";
358
- }
359
-
360
- function processTag(rawTag = "") {
361
- const source = String(rawTag || "");
362
- const inner = source.slice(1, -1).trim();
363
- const isClosing = inner.startsWith("/");
364
- const isSelfClosing = inner.endsWith("/");
365
- const tagType = resolveTagType(inner);
366
-
367
- if (suppressedDepth > 0) {
368
- if (tagType && isClosing) {
369
- suppressedDepth = Math.max(0, suppressedDepth - 1);
370
- } else if (tagType && !isClosing && !isSelfClosing) {
371
- suppressedDepth += 1;
372
- }
373
- return "";
374
- }
375
-
376
- if (!tagType) {
377
- return source;
378
- }
379
-
380
- if (!isClosing && !isSelfClosing) {
381
- suppressedDepth = 1;
382
- }
383
- return "";
384
- }
385
-
386
- function process(delta = "") {
387
- const source = String(delta || "");
388
- if (!source) {
389
- return "";
390
- }
391
-
392
- let output = "";
393
- for (const char of source) {
394
- if (inTag) {
395
- tagBuffer += char;
396
- if (char === ">") {
397
- inTag = false;
398
- output += processTag(tagBuffer);
399
- tagBuffer = "";
400
- }
401
- continue;
402
- }
403
-
404
- if (char === "<") {
405
- inTag = true;
406
- tagBuffer = "<";
407
- continue;
408
- }
409
-
410
- if (suppressedDepth < 1) {
411
- output += char;
412
- }
413
- }
414
-
415
- return output;
416
- }
417
-
418
- function flush() {
419
- return "";
420
- }
421
-
422
- return Object.freeze({
423
- process,
424
- flush
425
- });
426
- }
427
-
428
- async function consumeCompletionStream({ stream, streamWriter, emitDeltas = true, deltaSanitizer = null } = {}) {
429
- let assistantText = "";
430
- let streamedAssistantText = "";
431
- const toolCallsByIndex = new Map();
432
-
433
- for await (const chunk of stream) {
434
- const choice = chunk?.choices?.[0] || {};
435
- const delta = choice?.delta || {};
436
-
437
- const textDelta = extractTextDelta(delta.content);
438
- if (textDelta) {
439
- assistantText += textDelta;
440
- if (emitDeltas) {
441
- const safeDelta =
442
- deltaSanitizer && typeof deltaSanitizer.process === "function"
443
- ? String(deltaSanitizer.process(textDelta) || "")
444
- : textDelta;
445
- if (safeDelta) {
446
- streamedAssistantText += safeDelta;
447
- streamWriter.sendAssistantDelta({
448
- type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA,
449
- delta: safeDelta
450
- });
451
- }
452
- }
453
- }
454
-
455
- const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
456
- for (const partialToolCall of toolCalls) {
457
- const index = Number(partialToolCall?.index || 0);
458
- const existing =
459
- toolCallsByIndex.get(index) ||
460
- {
461
- id: normalizeText(partialToolCall?.id) || `tool_call_${index + 1}`,
462
- name: "",
463
- arguments: ""
464
- };
465
-
466
- if (partialToolCall?.id) {
467
- existing.id = normalizeText(partialToolCall.id) || existing.id;
468
- }
469
- if (partialToolCall?.function?.name) {
470
- existing.name += String(partialToolCall.function.name || "");
471
- }
472
- if (partialToolCall?.function?.arguments) {
473
- existing.arguments += String(partialToolCall.function.arguments || "");
474
- }
475
-
476
- toolCallsByIndex.set(index, existing);
477
- }
478
- }
479
-
480
- let toolCalls = [...toolCallsByIndex.values()].map((toolCall, index) => ({
481
- id: normalizeText(toolCall.id) || `tool_call_${index + 1}`,
482
- name: normalizeText(toolCall.name),
483
- arguments: String(toolCall.arguments || "")
484
- }));
485
-
486
- if (toolCalls.length < 1) {
487
- const parsedDsmlCalls = parseDsmlToolCallsFromText(assistantText);
488
- if (parsedDsmlCalls.length > 0) {
489
- toolCalls = parsedDsmlCalls;
490
- assistantText = normalizeText(sanitizeAssistantMessageText(assistantText));
491
- }
492
- }
493
-
494
- if (emitDeltas && deltaSanitizer && typeof deltaSanitizer.flush === "function") {
495
- const trailing = String(deltaSanitizer.flush() || "");
496
- if (trailing) {
497
- streamedAssistantText += trailing;
498
- streamWriter.sendAssistantDelta({
499
- type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA,
500
- delta: trailing
501
- });
502
- }
503
- }
504
-
505
- return {
506
- assistantText,
507
- streamedAssistantText,
508
- toolCalls
509
- };
510
- }
511
-
512
- function mergeAssistantMessageText(streamedText = "", completionText = "") {
513
- const streamed = normalizeText(sanitizeAssistantMessageText(streamedText));
514
- const completion = normalizeText(sanitizeAssistantMessageText(completionText));
515
-
516
- if (!streamed) {
517
- return completion;
518
- }
519
- if (!completion) {
520
- return streamed;
521
- }
522
- if (streamed === completion) {
523
- return streamed;
524
- }
525
- if (completion.startsWith(streamed) || completion.includes(streamed)) {
526
- return completion;
527
- }
528
- if (streamed.startsWith(completion) || streamed.includes(completion)) {
529
- return streamed;
530
- }
531
-
532
- return `${streamed}\n${completion}`;
533
- }
534
-
535
- function createChatService({ aiClient, transcriptService, serviceToolCatalog, assistantConfigService } = {}) {
536
- if (!aiClient || !transcriptService || !serviceToolCatalog || !assistantConfigService) {
537
- throw new Error("createChatService requires aiClient, transcriptService, serviceToolCatalog, and assistantConfigService.");
538
- }
539
-
540
- async function streamChat(payload = {}, options = {}) {
541
- if (!aiClient.enabled) {
542
- throw new AppError(503, "Assistant provider is not configured.");
543
- }
544
-
545
- const context = normalizeObject(options.context);
546
- const workspace = assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace ? resolveWorkspace(context, payload) : null;
547
- const source = normalizeStreamInput(payload);
548
- const streamWriter = options.streamWriter;
549
- if (!hasStreamWriter(streamWriter)) {
550
- throw new Error("assistant.chat.stream requires streamWriter methods.");
551
- }
552
-
553
- const actor = context.actor;
554
-
555
- const conversationResult = await transcriptService.createConversationForTurn(
556
- workspace,
557
- actor,
558
- {
559
- conversationId: source.conversationId,
560
- provider: aiClient.provider,
561
- model: aiClient.defaultModel,
562
- surfaceId: assistantRuntimeConfig.runtimeSurfaceId,
563
- messageId: source.messageId
564
- },
565
- {
566
- context
567
- }
568
- );
569
-
570
- const conversation = conversationResult.conversation;
571
- const conversationId = conversation?.id;
572
- if (!conversationId) {
573
- throw new AppError(500, "Assistant failed to create conversation.");
574
- }
575
-
576
- await transcriptService.appendMessage(
577
- conversationId,
578
- {
579
- role: "user",
580
- kind: "chat",
581
- clientMessageSid: source.messageId,
582
- contentText: source.input,
583
- metadata: {
584
- surfaceId: assistantRuntimeConfig.runtimeSurfaceId
585
- }
586
- },
587
- {
588
- context,
589
- workspace
590
- }
591
- );
592
-
593
- const toolSet = serviceToolCatalog.resolveToolSet(context);
594
- const customSystemPrompt = await assistantConfigService.resolveSystemPrompt(
595
- workspace,
596
- {
597
- surface: assistantRuntimeConfig.runtimeSurfaceId
598
- },
599
- {
600
- context,
601
- input: payload
602
- }
603
- );
604
- const systemPrompt = buildSystemPrompt({
605
- toolDescriptors: toolSet.tools,
606
- workspaceSlug: resolveWorkspaceSlug(context, payload),
607
- customSystemPrompt
608
- });
609
-
610
- const messages = [
611
- {
612
- role: "system",
613
- content: systemPrompt
614
- },
615
- ...source.history,
616
- {
617
- role: "user",
618
- content: source.input
619
- }
620
- ];
621
- let streamedAssistantText = "";
622
-
623
- async function completeWithAssistantMessage(assistantMessageText, { metadata = {} } = {}) {
624
- const normalizedAssistantMessageText = mergeAssistantMessageText(streamedAssistantText, assistantMessageText);
625
- if (!normalizedAssistantMessageText) {
626
- throw new AppError(502, "Assistant returned no output.");
627
- }
628
-
629
- await transcriptService.appendMessage(
630
- conversationId,
631
- {
632
- role: "assistant",
633
- kind: "chat",
634
- contentText: normalizedAssistantMessageText
635
- },
636
- {
637
- context,
638
- workspace
639
- }
640
- );
641
-
642
- await transcriptService.completeConversation(
643
- conversationId,
644
- {
645
- status: "completed",
646
- metadata
647
- },
648
- {
649
- context,
650
- workspace
651
- }
652
- );
653
-
654
- streamWriter.sendAssistantMessage({
655
- type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE,
656
- text: normalizedAssistantMessageText
657
- });
658
- streamWriter.sendDone({
659
- type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
660
- messageId: source.messageId,
661
- status: "completed"
662
- });
663
-
664
- return {
665
- conversationId,
666
- messageId: source.messageId,
667
- status: "completed"
668
- };
669
- }
670
-
671
- async function executeToolCalls(toolCalls = [], { toolFailures = [], toolSuccesses = [] } = {}) {
672
- const roundFailures = [];
673
-
674
- for (const toolCall of toolCalls) {
675
- streamWriter.sendToolCall({
676
- type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_CALL,
677
- toolCallId: toolCall.id,
678
- name: toolCall.name,
679
- arguments: toolCall.arguments
680
- });
681
-
682
- await transcriptService.appendMessage(
683
- conversationId,
684
- {
685
- role: "assistant",
686
- kind: "tool_call",
687
- contentText: toolCall.arguments,
688
- metadata: {
689
- toolCallId: toolCall.id,
690
- tool: toolCall.name
691
- }
692
- },
693
- {
694
- context,
695
- workspace
696
- }
697
- );
698
-
699
- const toolResult = await serviceToolCatalog.executeToolCall({
700
- toolName: toolCall.name,
701
- argumentsText: toolCall.arguments,
702
- context,
703
- toolSet
704
- });
705
-
706
- await transcriptService.appendMessage(
707
- conversationId,
708
- {
709
- role: "assistant",
710
- kind: "tool_result",
711
- contentText: JSON.stringify(toolResult),
712
- metadata: {
713
- toolCallId: toolCall.id,
714
- tool: toolCall.name,
715
- ok: toolResult.ok === true
716
- }
717
- },
718
- {
719
- context,
720
- workspace
721
- }
722
- );
723
-
724
- if (toolResult.ok) {
725
- toolSuccesses.push({
726
- name: toolCall.name,
727
- result: toolResult.result
728
- });
729
- streamWriter.sendToolResult({
730
- type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
731
- toolCallId: toolCall.id,
732
- name: toolCall.name,
733
- ok: true,
734
- result: toolResult.result
735
- });
736
-
737
- messages.push({
738
- role: "tool",
739
- tool_call_id: toolCall.id,
740
- content: JSON.stringify(toolResult.result ?? null)
741
- });
742
- continue;
743
- }
744
-
745
- const failure = {
746
- name: toolCall.name,
747
- error: toolResult.error
748
- };
749
- roundFailures.push(failure);
750
- toolFailures.push(failure);
751
-
752
- streamWriter.sendToolResult({
753
- type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
754
- toolCallId: toolCall.id,
755
- name: toolCall.name,
756
- ok: false,
757
- error: toolResult.error
758
- });
759
-
760
- messages.push({
761
- role: "tool",
762
- tool_call_id: toolCall.id,
763
- content: JSON.stringify({
764
- error: toolResult.error
765
- })
766
- });
767
- }
768
-
769
- return roundFailures;
770
- }
771
-
772
- async function recoverWithoutTools({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
773
- const MAX_RECOVERY_PASSES = 3;
774
- for (let pass = 0; pass < MAX_RECOVERY_PASSES; pass += 1) {
775
- const recoveryMessages = [
776
- ...messages,
777
- {
778
- role: "system",
779
- content: buildRecoveryPrompt({
780
- reason,
781
- toolFailures,
782
- toolSuccesses
783
- })
784
- }
785
- ];
786
-
787
- const completionStream = await aiClient.createChatCompletionStream({
788
- messages: recoveryMessages,
789
- tools: [],
790
- signal: options.abortSignal
791
- });
792
- const completion = await consumeCompletionStream({
793
- stream: completionStream,
794
- streamWriter,
795
- emitDeltas: true,
796
- deltaSanitizer: createDsmlDeltaSanitizer()
797
- });
798
- streamedAssistantText += String(completion.streamedAssistantText || "");
799
-
800
- const recoveryToolCalls = completion.toolCalls.filter((entry) => entry.name);
801
- if (recoveryToolCalls.length > 0) {
802
- messages.push(
803
- buildAssistantToolCallMessage({
804
- assistantText: completion.assistantText,
805
- toolCalls: recoveryToolCalls
806
- })
807
- );
808
- await executeToolCalls(recoveryToolCalls, {
809
- toolFailures,
810
- toolSuccesses
811
- });
812
- continue;
813
- }
814
-
815
- const assistantMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
816
- if (assistantMessageText) {
817
- return completeWithAssistantMessage(assistantMessageText, {
818
- metadata: {
819
- recoveryReason: reason || "unknown",
820
- toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
821
- }
822
- });
823
- }
824
- }
825
-
826
- const fallbackText = buildRecoveryFallbackAnswer({
827
- reason,
828
- toolFailures,
829
- toolSuccesses
830
- });
831
- return completeWithAssistantMessage(fallbackText, {
832
- metadata: {
833
- recoveryReason: reason || "unknown",
834
- toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
835
- }
836
- });
837
- }
838
-
839
- let streamed = false;
840
-
841
- try {
842
- streamWriter.sendMeta({
843
- type: ASSISTANT_STREAM_EVENT_TYPES.META,
844
- messageId: source.messageId,
845
- conversationId,
846
- provider: aiClient.provider,
847
- model: aiClient.defaultModel
848
- });
849
- streamed = true;
850
-
851
- const excludedToolNames = new Set();
852
- const toolFailures = [];
853
- const toolSuccesses = [];
854
-
855
- for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
856
- const roundToolDescriptors = toolSet.tools.filter(
857
- (tool) => !excludedToolNames.has(normalizeText(tool.name))
858
- );
859
- const roundToolSchemas = roundToolDescriptors.map((tool) => serviceToolCatalog.toOpenAiToolSchema(tool));
860
-
861
- const completionStream = await aiClient.createChatCompletionStream({
862
- messages,
863
- tools: roundToolSchemas,
864
- signal: options.abortSignal
865
- });
866
-
867
- const completion = await consumeCompletionStream({
868
- stream: completionStream,
869
- streamWriter,
870
- emitDeltas: true,
871
- deltaSanitizer: createDsmlDeltaSanitizer()
872
- });
873
- streamedAssistantText += String(completion.streamedAssistantText || "");
874
-
875
- const toolCalls = completion.toolCalls.filter((entry) => entry.name);
876
- if (toolCalls.length < 1) {
877
- const finalMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
878
- if (finalMessageText) {
879
- return completeWithAssistantMessage(finalMessageText, {
880
- metadata: toolFailures.length > 0
881
- ? {
882
- recoveryReason: "tool_failure",
883
- toolFailureCount: toolFailures.length
884
- }
885
- : {}
886
- });
887
- }
888
-
889
- if (toolFailures.length > 0) {
890
- return recoverWithoutTools({
891
- reason: "tool_failure",
892
- toolFailures,
893
- toolSuccesses
894
- });
895
- }
896
-
897
- return completeWithAssistantMessage(completion.assistantText);
898
- }
899
-
900
- messages.push(
901
- buildAssistantToolCallMessage({
902
- assistantText: completion.assistantText,
903
- toolCalls
904
- })
905
- );
906
-
907
- const roundFailures = await executeToolCalls(toolCalls, {
908
- toolFailures,
909
- toolSuccesses
910
- });
911
-
912
- if (roundFailures.length > 0) {
913
- for (const failure of roundFailures) {
914
- const toolName = normalizeText(failure?.name);
915
- if (toolName) {
916
- excludedToolNames.add(toolName);
917
- }
918
- }
919
- }
920
- }
921
-
922
- return recoverWithoutTools({
923
- reason: toolFailures.length > 0 ? "tool_failure" : "max_tool_rounds",
924
- toolFailures,
925
- toolSuccesses
926
- });
927
- } catch (error) {
928
- const aborted = isAbortError(error);
929
- const status = aborted ? "aborted" : "failed";
930
-
931
- if (streamed) {
932
- await transcriptService.completeConversation(
933
- conversationId,
934
- {
935
- status
936
- },
937
- {
938
- context,
939
- workspace
940
- }
941
- );
942
-
943
- streamWriter.sendError({
944
- type: ASSISTANT_STREAM_EVENT_TYPES.ERROR,
945
- messageId: source.messageId,
946
- code: aborted ? "assistant_stream_aborted" : String(error?.code || "assistant_stream_failed"),
947
- message: aborted ? "Assistant request was cancelled." : String(error?.message || "Assistant request failed."),
948
- status: aborted ? 499 : Number(error?.status || error?.statusCode || 500)
949
- });
950
-
951
- streamWriter.sendDone({
952
- type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
953
- messageId: source.messageId,
954
- status
955
- });
956
-
957
- return {
958
- conversationId,
959
- messageId: source.messageId,
960
- status
961
- };
962
- }
963
-
964
- throw error;
965
- }
966
- }
967
-
968
- async function listConversations(query = {}, options = {}) {
969
- const context = normalizeObject(options.context);
970
- const workspace = assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace
971
- ? resolveWorkspace(context, options.input || {})
972
- : null;
973
- return transcriptService.listConversationsForUser(workspace, context.actor, query, {
974
- context
975
- });
976
- }
977
-
978
- async function getConversationMessages(conversationId, query = {}, options = {}) {
979
- const context = normalizeObject(options.context);
980
- const workspace = assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace
981
- ? resolveWorkspace(context, options.input || {})
982
- : null;
983
- return transcriptService.getConversationMessagesForUser(workspace, context.actor, conversationId, query, {
984
- context
985
- });
986
- }
987
-
988
- return Object.freeze({
989
- streamChat,
990
- listConversations,
991
- getConversationMessages
992
- });
993
- }
994
-
995
- export { createChatService };