@jskit-ai/assistant-runtime 0.1.143 → 0.1.145
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/package.json +7 -7
- package/src/client/composables/useAssistantRuntime.js +33 -125
- package/src/client/support/assistantRuntimeState.js +140 -0
- package/src/client/support/conversationRestoreSupport.js +75 -0
- package/src/server/services/chatService.js +93 -212
- package/src/shared/assistantResponseText.js +29 -0
- package/test/assistantRuntimeState.test.js +134 -0
- package/test/chatServiceLifecycle.test.js +415 -0
- package/test/conversationRestoreSupport.test.js +110 -0
|
@@ -5,10 +5,16 @@ import {
|
|
|
5
5
|
ASSISTANT_STREAM_EVENT_TYPES
|
|
6
6
|
} from "@jskit-ai/assistant-core/shared";
|
|
7
7
|
import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
|
|
8
|
+
import { isAssistantProgressOnlyText } from "../../shared/assistantResponseText.js";
|
|
8
9
|
|
|
9
10
|
const MAX_HISTORY_MESSAGES = 20;
|
|
10
11
|
const MAX_INPUT_CHARS = 8000;
|
|
11
|
-
const MAX_TOOL_ROUNDS =
|
|
12
|
+
const MAX_TOOL_ROUNDS = 16;
|
|
13
|
+
const MAX_RECOVERY_PASSES = 3;
|
|
14
|
+
const MAX_TOOL_RESULT_FALLBACK_CHARS = 4000;
|
|
15
|
+
const CURRENT_TIME_PREFLIGHT_INTENT = "current-time";
|
|
16
|
+
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.";
|
|
17
|
+
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.";
|
|
12
18
|
|
|
13
19
|
function normalizeConversationId(value) {
|
|
14
20
|
return normalizeRecordId(value, { fallback: null });
|
|
@@ -26,7 +32,7 @@ function normalizeHistory(history = []) {
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
const content = normalizeText(item.content).slice(0, MAX_INPUT_CHARS);
|
|
29
|
-
if (!content) {
|
|
35
|
+
if (!content || (role === "assistant" && isAssistantProgressOnlyText(content))) {
|
|
30
36
|
return null;
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -90,6 +96,36 @@ function isAbortError(error) {
|
|
|
90
96
|
return String(error.name || "").trim() === "AbortError";
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
function requiresCurrentTime(value = "") {
|
|
100
|
+
const text = normalizeText(value);
|
|
101
|
+
if (!text) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
/\b(?:now|today|tomorrow|yesterday|tonight)\b/iu,
|
|
107
|
+
/\b(?:current|local)\s+(?:date|day|time|date\s+and\s+time)\b/iu,
|
|
108
|
+
/\bwhat(?:'s|\s+is)\s+(?:the\s+)?(?:date|day|time)\b/iu,
|
|
109
|
+
/\b(?:this|next|last)\s+(?:day|week|month|year|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/iu
|
|
110
|
+
].some((pattern) => pattern.test(text));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolvePreflightTools(toolDescriptors = [], input = "") {
|
|
114
|
+
if (!requiresCurrentTime(input)) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const currentTimeTool = toolDescriptors.find((tool) => {
|
|
119
|
+
const intents = Array.isArray(tool?.preflight) ? tool.preflight : [];
|
|
120
|
+
const requiredParameters = Array.isArray(tool?.parameters?.required)
|
|
121
|
+
? tool.parameters.required
|
|
122
|
+
: [];
|
|
123
|
+
return requiredParameters.length < 1 && intents.includes(CURRENT_TIME_PREFLIGHT_INTENT);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return currentTimeTool ? [currentTimeTool] : [];
|
|
127
|
+
}
|
|
128
|
+
|
|
93
129
|
function extractTextDelta(deltaContent) {
|
|
94
130
|
if (typeof deltaContent === "string") {
|
|
95
131
|
return deltaContent;
|
|
@@ -152,6 +188,7 @@ function buildSystemPrompt({ targetSurfaceId = "", toolDescriptors = [], workspa
|
|
|
152
188
|
"Use tools when they are necessary and only when available.",
|
|
153
189
|
"Do not mention tools that are not available.",
|
|
154
190
|
"When answering schema questions, rely only on tool contracts and tool results.",
|
|
191
|
+
CLOCK_INSTRUCTION,
|
|
155
192
|
workspaceLine,
|
|
156
193
|
toolSummary,
|
|
157
194
|
toolContracts
|
|
@@ -183,22 +220,21 @@ function buildRecoveryPrompt({ reason = "", toolFailures = [], toolSuccesses = [
|
|
|
183
220
|
const failureSuffix = failureSummary ? ` Recent tool failures: ${failureSummary}.` : "";
|
|
184
221
|
const successSuffix = successSummary ? ` Successful tools: ${successSummary}.` : "";
|
|
185
222
|
if (normalizedReason === "tool_failure") {
|
|
186
|
-
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
|
|
223
|
+
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}`;
|
|
187
224
|
}
|
|
188
225
|
|
|
189
|
-
return `Tool-call rounds were exhausted. Provide the best direct answer with available context and successful results only
|
|
226
|
+
return `Tool-call rounds were exhausted. Provide the best direct answer with available context and successful results only. ${COMPLETION_INSTRUCTION}${failureSuffix}${successSuffix}`;
|
|
190
227
|
}
|
|
191
228
|
|
|
192
229
|
function buildRecoveryFallbackAnswer({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return buildToolOutcomeFallbackAnswer({
|
|
196
|
-
toolFailures,
|
|
197
|
-
toolSuccesses
|
|
198
|
-
});
|
|
230
|
+
if (normalizeText(reason).toLowerCase() === "max_tool_rounds") {
|
|
231
|
+
return "Limit reached. Start a new conversation.";
|
|
199
232
|
}
|
|
200
233
|
|
|
201
|
-
return
|
|
234
|
+
return buildToolOutcomeFallbackAnswer({
|
|
235
|
+
toolFailures,
|
|
236
|
+
toolSuccesses
|
|
237
|
+
});
|
|
202
238
|
}
|
|
203
239
|
|
|
204
240
|
function toSafeToolResultText(value) {
|
|
@@ -217,26 +253,19 @@ function toSafeToolResultText(value) {
|
|
|
217
253
|
}
|
|
218
254
|
|
|
219
255
|
function buildToolOutcomeFallbackAnswer({ toolFailures = [], toolSuccesses = [] } = {}) {
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
.map((entry) => normalizeText(entry?.name))
|
|
223
|
-
.filter(Boolean)
|
|
224
|
-
)];
|
|
256
|
+
const successfulResults = (Array.isArray(toolSuccesses) ? toolSuccesses : [])
|
|
257
|
+
.filter((entry) => normalizeText(entry?.name));
|
|
225
258
|
const hasFailures = Array.isArray(toolFailures) && toolFailures.length > 0;
|
|
226
259
|
|
|
227
|
-
if (
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return `- ${name}:\n\`\`\`json\n${payload}\n\`\`\``;
|
|
234
|
-
});
|
|
260
|
+
if (successfulResults.length > 0) {
|
|
261
|
+
const latestSuccess = successfulResults.at(-1);
|
|
262
|
+
const answer = `Latest successful result from ${normalizeText(latestSuccess.name)}:\n${toSafeToolResultText(latestSuccess.result)}`;
|
|
263
|
+
if (answer.length <= MAX_TOOL_RESULT_FALLBACK_CHARS) {
|
|
264
|
+
return answer;
|
|
265
|
+
}
|
|
235
266
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
...summaryLines
|
|
239
|
-
].join("\n");
|
|
267
|
+
const suffix = "\n…[truncated]";
|
|
268
|
+
return `${answer.slice(0, MAX_TOOL_RESULT_FALLBACK_CHARS - suffix.length)}${suffix}`;
|
|
240
269
|
}
|
|
241
270
|
|
|
242
271
|
if (hasFailures) {
|
|
@@ -255,7 +284,8 @@ function sanitizeAssistantMessageText(value) {
|
|
|
255
284
|
const blockPatterns = [
|
|
256
285
|
/<[^>\n]*function_calls[^>\n]*>[\s\S]*?<\/[^>\n]*function_calls>/gi,
|
|
257
286
|
/<[^>\n]*tool_calls?[^>\n]*>[\s\S]*?<\/[^>\n]*tool_calls?[^>\n]*>/gi,
|
|
258
|
-
/<[^>\n]*invoke\b[^>\n]*>[\s\S]*?<\/[^>\n]*invoke>/gi
|
|
287
|
+
/<[^>\n]*invoke\b[^>\n]*>[\s\S]*?<\/[^>\n]*invoke>/gi,
|
|
288
|
+
/<(?:analysis|reasoning|think)>[\s\S]*?<\/(?:analysis|reasoning|think)>/gi
|
|
259
289
|
];
|
|
260
290
|
for (const pattern of blockPatterns) {
|
|
261
291
|
source = source.replace(pattern, " ");
|
|
@@ -279,10 +309,10 @@ function sanitizeAssistantMessageText(value) {
|
|
|
279
309
|
.join("\n");
|
|
280
310
|
}
|
|
281
311
|
|
|
282
|
-
function buildAssistantToolCallMessage(
|
|
312
|
+
function buildAssistantToolCallMessage(toolCalls = []) {
|
|
283
313
|
return {
|
|
284
314
|
role: "assistant",
|
|
285
|
-
content:
|
|
315
|
+
content: "",
|
|
286
316
|
tool_calls: toolCalls.map((toolCall) => ({
|
|
287
317
|
id: toolCall.id,
|
|
288
318
|
type: "function",
|
|
@@ -336,96 +366,8 @@ function parseDsmlToolCallsFromText(value = "") {
|
|
|
336
366
|
return calls;
|
|
337
367
|
}
|
|
338
368
|
|
|
339
|
-
function
|
|
340
|
-
let inTag = false;
|
|
341
|
-
let tagBuffer = "";
|
|
342
|
-
let suppressedDepth = 0;
|
|
343
|
-
|
|
344
|
-
function resolveTagType(rawTag = "") {
|
|
345
|
-
const normalizedTag = String(rawTag || "").toLowerCase();
|
|
346
|
-
if (normalizedTag.includes("function_calls")) {
|
|
347
|
-
return "function_calls";
|
|
348
|
-
}
|
|
349
|
-
if (normalizedTag.includes("tool_calls")) {
|
|
350
|
-
return "tool_calls";
|
|
351
|
-
}
|
|
352
|
-
if (normalizedTag.includes("invoke")) {
|
|
353
|
-
return "invoke";
|
|
354
|
-
}
|
|
355
|
-
return "";
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function processTag(rawTag = "") {
|
|
359
|
-
const source = String(rawTag || "");
|
|
360
|
-
const inner = source.slice(1, -1).trim();
|
|
361
|
-
const isClosing = inner.startsWith("/");
|
|
362
|
-
const isSelfClosing = inner.endsWith("/");
|
|
363
|
-
const tagType = resolveTagType(inner);
|
|
364
|
-
|
|
365
|
-
if (suppressedDepth > 0) {
|
|
366
|
-
if (tagType && isClosing) {
|
|
367
|
-
suppressedDepth = Math.max(0, suppressedDepth - 1);
|
|
368
|
-
} else if (tagType && !isClosing && !isSelfClosing) {
|
|
369
|
-
suppressedDepth += 1;
|
|
370
|
-
}
|
|
371
|
-
return "";
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (!tagType) {
|
|
375
|
-
return source;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (!isClosing && !isSelfClosing) {
|
|
379
|
-
suppressedDepth = 1;
|
|
380
|
-
}
|
|
381
|
-
return "";
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function process(delta = "") {
|
|
385
|
-
const source = String(delta || "");
|
|
386
|
-
if (!source) {
|
|
387
|
-
return "";
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
let output = "";
|
|
391
|
-
for (const char of source) {
|
|
392
|
-
if (inTag) {
|
|
393
|
-
tagBuffer += char;
|
|
394
|
-
if (char === ">") {
|
|
395
|
-
inTag = false;
|
|
396
|
-
output += processTag(tagBuffer);
|
|
397
|
-
tagBuffer = "";
|
|
398
|
-
}
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (char === "<") {
|
|
403
|
-
inTag = true;
|
|
404
|
-
tagBuffer = "<";
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (suppressedDepth < 1) {
|
|
409
|
-
output += char;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return output;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function flush() {
|
|
417
|
-
return "";
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return Object.freeze({
|
|
421
|
-
process,
|
|
422
|
-
flush
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
async function consumeCompletionStream({ stream, streamWriter, emitDeltas = true, deltaSanitizer = null } = {}) {
|
|
369
|
+
async function consumeCompletionStream(stream) {
|
|
427
370
|
let assistantText = "";
|
|
428
|
-
let streamedAssistantText = "";
|
|
429
371
|
const toolCallsByIndex = new Map();
|
|
430
372
|
|
|
431
373
|
for await (const chunk of stream) {
|
|
@@ -435,19 +377,6 @@ async function consumeCompletionStream({ stream, streamWriter, emitDeltas = true
|
|
|
435
377
|
const textDelta = extractTextDelta(delta.content);
|
|
436
378
|
if (textDelta) {
|
|
437
379
|
assistantText += textDelta;
|
|
438
|
-
if (emitDeltas) {
|
|
439
|
-
const safeDelta =
|
|
440
|
-
deltaSanitizer && typeof deltaSanitizer.process === "function"
|
|
441
|
-
? String(deltaSanitizer.process(textDelta) || "")
|
|
442
|
-
: textDelta;
|
|
443
|
-
if (safeDelta) {
|
|
444
|
-
streamedAssistantText += safeDelta;
|
|
445
|
-
streamWriter.sendAssistantDelta({
|
|
446
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA,
|
|
447
|
-
delta: safeDelta
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
380
|
}
|
|
452
381
|
|
|
453
382
|
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
@@ -489,47 +418,12 @@ async function consumeCompletionStream({ stream, streamWriter, emitDeltas = true
|
|
|
489
418
|
}
|
|
490
419
|
}
|
|
491
420
|
|
|
492
|
-
if (emitDeltas && deltaSanitizer && typeof deltaSanitizer.flush === "function") {
|
|
493
|
-
const trailing = String(deltaSanitizer.flush() || "");
|
|
494
|
-
if (trailing) {
|
|
495
|
-
streamedAssistantText += trailing;
|
|
496
|
-
streamWriter.sendAssistantDelta({
|
|
497
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA,
|
|
498
|
-
delta: trailing
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
421
|
return {
|
|
504
422
|
assistantText,
|
|
505
|
-
streamedAssistantText,
|
|
506
423
|
toolCalls
|
|
507
424
|
};
|
|
508
425
|
}
|
|
509
426
|
|
|
510
|
-
function mergeAssistantMessageText(streamedText = "", completionText = "") {
|
|
511
|
-
const streamed = normalizeText(sanitizeAssistantMessageText(streamedText));
|
|
512
|
-
const completion = normalizeText(sanitizeAssistantMessageText(completionText));
|
|
513
|
-
|
|
514
|
-
if (!streamed) {
|
|
515
|
-
return completion;
|
|
516
|
-
}
|
|
517
|
-
if (!completion) {
|
|
518
|
-
return streamed;
|
|
519
|
-
}
|
|
520
|
-
if (streamed === completion) {
|
|
521
|
-
return streamed;
|
|
522
|
-
}
|
|
523
|
-
if (completion.startsWith(streamed) || completion.includes(streamed)) {
|
|
524
|
-
return completion;
|
|
525
|
-
}
|
|
526
|
-
if (streamed.startsWith(completion) || streamed.includes(completion)) {
|
|
527
|
-
return streamed;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return `${streamed}\n${completion}`;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
427
|
function requireAssistantSurface(appConfig = {}, targetSurfaceId = "") {
|
|
534
428
|
const assistantSurface = resolveAssistantSurfaceConfig(appConfig, targetSurfaceId);
|
|
535
429
|
if (assistantSurface) {
|
|
@@ -664,10 +558,9 @@ function createChatService({
|
|
|
664
558
|
content: source.input
|
|
665
559
|
}
|
|
666
560
|
];
|
|
667
|
-
let streamedAssistantText = "";
|
|
668
561
|
|
|
669
562
|
async function completeWithAssistantMessage(assistantMessageText, { metadata = {} } = {}) {
|
|
670
|
-
const normalizedAssistantMessageText =
|
|
563
|
+
const normalizedAssistantMessageText = normalizeText(sanitizeAssistantMessageText(assistantMessageText));
|
|
671
564
|
if (!normalizedAssistantMessageText) {
|
|
672
565
|
throw new AppError(502, "Assistant returned no output.");
|
|
673
566
|
}
|
|
@@ -820,7 +713,6 @@ function createChatService({
|
|
|
820
713
|
}
|
|
821
714
|
|
|
822
715
|
async function recoverWithoutTools({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
|
|
823
|
-
const MAX_RECOVERY_PASSES = 3;
|
|
824
716
|
for (let pass = 0; pass < MAX_RECOVERY_PASSES; pass += 1) {
|
|
825
717
|
const recoveryMessages = [
|
|
826
718
|
...messages,
|
|
@@ -839,31 +731,15 @@ function createChatService({
|
|
|
839
731
|
tools: [],
|
|
840
732
|
signal: options.abortSignal
|
|
841
733
|
});
|
|
842
|
-
const completion = await consumeCompletionStream(
|
|
843
|
-
stream: completionStream,
|
|
844
|
-
streamWriter,
|
|
845
|
-
emitDeltas: true,
|
|
846
|
-
deltaSanitizer: createDsmlDeltaSanitizer()
|
|
847
|
-
});
|
|
848
|
-
streamedAssistantText += String(completion.streamedAssistantText || "");
|
|
734
|
+
const completion = await consumeCompletionStream(completionStream);
|
|
849
735
|
|
|
850
736
|
const recoveryToolCalls = completion.toolCalls.filter((entry) => entry.name);
|
|
851
737
|
if (recoveryToolCalls.length > 0) {
|
|
852
|
-
messages.push(
|
|
853
|
-
buildAssistantToolCallMessage({
|
|
854
|
-
assistantText: completion.assistantText,
|
|
855
|
-
toolCalls: recoveryToolCalls
|
|
856
|
-
})
|
|
857
|
-
);
|
|
858
|
-
await executeToolCalls(recoveryToolCalls, {
|
|
859
|
-
toolFailures,
|
|
860
|
-
toolSuccesses
|
|
861
|
-
});
|
|
862
738
|
continue;
|
|
863
739
|
}
|
|
864
740
|
|
|
865
741
|
const assistantMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
|
|
866
|
-
if (assistantMessageText) {
|
|
742
|
+
if (assistantMessageText && !isAssistantProgressOnlyText(assistantMessageText)) {
|
|
867
743
|
return completeWithAssistantMessage(assistantMessageText, {
|
|
868
744
|
metadata: {
|
|
869
745
|
recoveryReason: reason || "unknown",
|
|
@@ -902,6 +778,26 @@ function createChatService({
|
|
|
902
778
|
const toolFailures = [];
|
|
903
779
|
const toolSuccesses = [];
|
|
904
780
|
|
|
781
|
+
const preflightTools = resolvePreflightTools(toolSet.tools, source.input);
|
|
782
|
+
for (const [index, tool] of preflightTools.entries()) {
|
|
783
|
+
const toolCall = {
|
|
784
|
+
id: `assistant_preflight_${index + 1}`,
|
|
785
|
+
name: tool.name,
|
|
786
|
+
arguments: "{}"
|
|
787
|
+
};
|
|
788
|
+
messages.push(buildAssistantToolCallMessage([toolCall]));
|
|
789
|
+
const preflightFailures = await executeToolCalls([toolCall], {
|
|
790
|
+
toolFailures,
|
|
791
|
+
toolSuccesses
|
|
792
|
+
});
|
|
793
|
+
for (const failure of preflightFailures) {
|
|
794
|
+
const toolName = normalizeText(failure?.name);
|
|
795
|
+
if (toolName) {
|
|
796
|
+
excludedToolNames.add(toolName);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
905
801
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
906
802
|
const roundToolDescriptors = toolSet.tools.filter(
|
|
907
803
|
(tool) => !excludedToolNames.has(normalizeText(tool.name))
|
|
@@ -914,18 +810,12 @@ function createChatService({
|
|
|
914
810
|
signal: options.abortSignal
|
|
915
811
|
});
|
|
916
812
|
|
|
917
|
-
const completion = await consumeCompletionStream(
|
|
918
|
-
stream: completionStream,
|
|
919
|
-
streamWriter,
|
|
920
|
-
emitDeltas: true,
|
|
921
|
-
deltaSanitizer: createDsmlDeltaSanitizer()
|
|
922
|
-
});
|
|
923
|
-
streamedAssistantText += String(completion.streamedAssistantText || "");
|
|
813
|
+
const completion = await consumeCompletionStream(completionStream);
|
|
924
814
|
|
|
925
815
|
const toolCalls = completion.toolCalls.filter((entry) => entry.name);
|
|
926
816
|
if (toolCalls.length < 1) {
|
|
927
817
|
const finalMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
|
|
928
|
-
if (finalMessageText) {
|
|
818
|
+
if (finalMessageText && !isAssistantProgressOnlyText(finalMessageText)) {
|
|
929
819
|
return completeWithAssistantMessage(finalMessageText, {
|
|
930
820
|
metadata: toolFailures.length > 0
|
|
931
821
|
? {
|
|
@@ -936,23 +826,14 @@ function createChatService({
|
|
|
936
826
|
});
|
|
937
827
|
}
|
|
938
828
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
return completeWithAssistantMessage(completion.assistantText);
|
|
829
|
+
messages.push({
|
|
830
|
+
role: "system",
|
|
831
|
+
content: COMPLETION_INSTRUCTION
|
|
832
|
+
});
|
|
833
|
+
continue;
|
|
948
834
|
}
|
|
949
835
|
|
|
950
|
-
messages.push(
|
|
951
|
-
buildAssistantToolCallMessage({
|
|
952
|
-
assistantText: completion.assistantText,
|
|
953
|
-
toolCalls
|
|
954
|
-
})
|
|
955
|
-
);
|
|
836
|
+
messages.push(buildAssistantToolCallMessage(toolCalls));
|
|
956
837
|
|
|
957
838
|
const roundFailures = await executeToolCalls(toolCalls, {
|
|
958
839
|
toolFailures,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const MAX_PROGRESS_ONLY_TEXT_CHARS = 600;
|
|
2
|
+
|
|
3
|
+
const PROGRESS_SENTENCE_PATTERNS = Object.freeze([
|
|
4
|
+
/^(?:let me|i(?:'|’)ll|i will|i(?:'|’)m going to|i am going to)\s+(?:(?:first|now|quickly)\s+)*(?:analy[sz]e|call|check|confirm|do|execute|fetch|find|inspect|investigate|load|look up|open|prepare|query|read|retrieve|review|run|search|summarize|test|try|use|verify)\b/iu,
|
|
5
|
+
/^(?:analy[sz]ing|calling|checking|confirming|executing|fetching|finding|inspecting|investigating|loading|looking up|opening|preparing|querying|reading|retrieving|reviewing|running|searching|summarizing|testing|trying|using|verifying)\b/iu,
|
|
6
|
+
/^(?:one moment|please wait)\b/iu
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
function isAssistantProgressOnlyText(value) {
|
|
10
|
+
const text = String(value || "")
|
|
11
|
+
.replace(/\s+/gu, " ")
|
|
12
|
+
.trim()
|
|
13
|
+
.replace(/^(?:okay|sure)[,;:!\s—-]+/iu, "");
|
|
14
|
+
|
|
15
|
+
if (!text || text.length > MAX_PROGRESS_ONLY_TEXT_CHARS) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sentences = text
|
|
20
|
+
.split(/(?<=[.!?…])\s+/u)
|
|
21
|
+
.map((sentence) => sentence.trim())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
|
|
24
|
+
return sentences.length > 0 && sentences.every((sentence) =>
|
|
25
|
+
PROGRESS_SENTENCE_PATTERNS.some((pattern) => pattern.test(sentence))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { isAssistantProgressOnlyText };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildHistory,
|
|
5
|
+
interruptPendingToolEvents,
|
|
6
|
+
mapTranscriptEntriesToAssistantState
|
|
7
|
+
} from "../src/client/support/assistantRuntimeState.js";
|
|
8
|
+
|
|
9
|
+
test("restored progress narration is neither rendered nor replayed", () => {
|
|
10
|
+
const restored = mapTranscriptEntriesToAssistantState([
|
|
11
|
+
{
|
|
12
|
+
id: "1",
|
|
13
|
+
role: "user",
|
|
14
|
+
kind: "chat",
|
|
15
|
+
contentText: "Find the booking."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "2",
|
|
19
|
+
role: "assistant",
|
|
20
|
+
kind: "chat",
|
|
21
|
+
contentText: "Let me query the bookings."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "3",
|
|
25
|
+
role: "assistant",
|
|
26
|
+
kind: "chat",
|
|
27
|
+
contentText: "The booking is confirmed."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "4",
|
|
31
|
+
role: "assistant",
|
|
32
|
+
kind: "chat",
|
|
33
|
+
contentText: "Let me check. The second booking is also confirmed."
|
|
34
|
+
}
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
restored.messages.map((message) => message.text),
|
|
39
|
+
[
|
|
40
|
+
"Find the booking.",
|
|
41
|
+
"The booking is confirmed.",
|
|
42
|
+
"Let me check. The second booking is also confirmed."
|
|
43
|
+
]
|
|
44
|
+
);
|
|
45
|
+
assert.deepEqual(buildHistory(restored.messages), [
|
|
46
|
+
{
|
|
47
|
+
role: "user",
|
|
48
|
+
content: "Find the booking."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
role: "assistant",
|
|
52
|
+
content: "The booking is confirmed."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
role: "assistant",
|
|
56
|
+
content: "Let me check. The second booking is also confirmed."
|
|
57
|
+
}
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("restored orphaned tool calls are marked interrupted", () => {
|
|
62
|
+
const restored = mapTranscriptEntriesToAssistantState([
|
|
63
|
+
{
|
|
64
|
+
id: "1",
|
|
65
|
+
role: "assistant",
|
|
66
|
+
kind: "tool_call",
|
|
67
|
+
contentText: "{}",
|
|
68
|
+
metadata: {
|
|
69
|
+
toolCallId: "orphaned",
|
|
70
|
+
tool: "action_search"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "2",
|
|
75
|
+
role: "assistant",
|
|
76
|
+
kind: "tool_call",
|
|
77
|
+
contentText: "{}",
|
|
78
|
+
metadata: {
|
|
79
|
+
toolCallId: "completed",
|
|
80
|
+
tool: "action_execute"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "3",
|
|
85
|
+
role: "assistant",
|
|
86
|
+
kind: "tool_result",
|
|
87
|
+
contentText: JSON.stringify({
|
|
88
|
+
ok: true,
|
|
89
|
+
result: {
|
|
90
|
+
id: "41"
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
metadata: {
|
|
94
|
+
toolCallId: "completed",
|
|
95
|
+
tool: "action_execute",
|
|
96
|
+
ok: true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(
|
|
102
|
+
restored.pendingToolEvents.map((event) => ({ id: event.id, status: event.status })),
|
|
103
|
+
[
|
|
104
|
+
{ id: "orphaned", status: "interrupted" },
|
|
105
|
+
{ id: "completed", status: "done" }
|
|
106
|
+
]
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("live stream cleanup interrupts only tool events that remain pending", () => {
|
|
111
|
+
const finalized = interruptPendingToolEvents([
|
|
112
|
+
{
|
|
113
|
+
id: "pending",
|
|
114
|
+
status: "pending"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "done",
|
|
118
|
+
status: "done"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "failed",
|
|
122
|
+
status: "failed"
|
|
123
|
+
}
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
assert.deepEqual(
|
|
127
|
+
finalized.map((event) => ({ id: event.id, status: event.status })),
|
|
128
|
+
[
|
|
129
|
+
{ id: "pending", status: "interrupted" },
|
|
130
|
+
{ id: "done", status: "done" },
|
|
131
|
+
{ id: "failed", status: "failed" }
|
|
132
|
+
]
|
|
133
|
+
);
|
|
134
|
+
});
|