@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.
@@ -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 = 4;
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.${failureSuffix}${successSuffix}`;
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.${failureSuffix}${successSuffix}`;
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
- const normalizedReason = normalizeText(reason).toLowerCase();
194
- if (normalizedReason === "tool_failure") {
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 "I reached the tool-call limit for this request. Please narrow the request and I will continue.";
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 successNames = [...new Set(
221
- (Array.isArray(toolSuccesses) ? toolSuccesses : [])
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 (successNames.length > 0) {
228
- const summaryLines = (Array.isArray(toolSuccesses) ? toolSuccesses : [])
229
- .filter((entry) => normalizeText(entry?.name))
230
- .map((entry) => {
231
- const name = normalizeText(entry.name);
232
- const payload = toSafeToolResultText(entry.result);
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
- return [
237
- "I used the available successful results:",
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({ assistantText = "", toolCalls = [] } = {}) {
312
+ function buildAssistantToolCallMessage(toolCalls = []) {
283
313
  return {
284
314
  role: "assistant",
285
- content: assistantText || "",
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 createDsmlDeltaSanitizer() {
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 = mergeAssistantMessageText(streamedAssistantText, assistantMessageText);
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
- if (toolFailures.length > 0) {
940
- return recoverWithoutTools({
941
- reason: "tool_failure",
942
- toolFailures,
943
- toolSuccesses
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
+ });