@assistant-ui/react 0.12.12 → 0.12.15

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 (43) hide show
  1. package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  2. package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.js +135 -27
  3. package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  4. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.d.ts.map +1 -1
  5. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js +143 -38
  6. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js.map +1 -1
  7. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.d.ts.map +1 -1
  8. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.js +21 -9
  9. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.js.map +1 -1
  10. package/dist/primitives/actionBar/ActionBarInteractionContext.d.ts +6 -0
  11. package/dist/primitives/actionBar/ActionBarInteractionContext.d.ts.map +1 -0
  12. package/dist/primitives/actionBar/ActionBarInteractionContext.js +5 -0
  13. package/dist/primitives/actionBar/ActionBarInteractionContext.js.map +1 -0
  14. package/dist/primitives/actionBar/ActionBarRoot.d.ts.map +1 -1
  15. package/dist/primitives/actionBar/ActionBarRoot.js +18 -4
  16. package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
  17. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts +2 -1
  18. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts.map +1 -1
  19. package/dist/primitives/actionBar/useActionBarFloatStatus.js +3 -2
  20. package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
  21. package/dist/primitives/actionBarMore/ActionBarMoreRoot.d.ts.map +1 -1
  22. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +35 -2
  23. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js.map +1 -1
  24. package/dist/utils/json/is-json-equal.d.ts +2 -0
  25. package/dist/utils/json/is-json-equal.d.ts.map +1 -0
  26. package/dist/utils/json/is-json-equal.js +31 -0
  27. package/dist/utils/json/is-json-equal.js.map +1 -0
  28. package/dist/utils/json/is-json.d.ts +1 -0
  29. package/dist/utils/json/is-json.d.ts.map +1 -1
  30. package/dist/utils/json/is-json.js +5 -3
  31. package/dist/utils/json/is-json.js.map +1 -1
  32. package/package.json +7 -7
  33. package/src/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.ts +179 -40
  34. package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.test.ts +230 -4
  35. package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.ts +191 -50
  36. package/src/legacy-runtime/runtime-cores/external-store/external-message-converter.ts +28 -10
  37. package/src/primitives/actionBar/ActionBarInteractionContext.ts +13 -0
  38. package/src/primitives/actionBar/ActionBarRoot.tsx +38 -8
  39. package/src/primitives/actionBar/useActionBarFloatStatus.ts +4 -1
  40. package/src/primitives/actionBarMore/ActionBarMoreRoot.tsx +52 -2
  41. package/src/tests/external-message-converter.test.ts +80 -0
  42. package/src/utils/json/is-json-equal.ts +48 -0
  43. package/src/utils/json/is-json.ts +6 -3
@@ -64,10 +64,12 @@ class AssistantCloudThreadHistoryAdapter implements ThreadHistoryAdapter {
64
64
  stepTimestamps?: StepTimestamp[];
65
65
  },
66
66
  ) {
67
- const encodedContents = items.map((item) => formatAdapter.encode(item));
68
- adapter._reportBatchTelemetry(
67
+ const encodedRunMessages = items.map((item) =>
68
+ formatAdapter.encode(item),
69
+ );
70
+ adapter._reportRunTelemetry(
69
71
  formatAdapter.format,
70
- encodedContents,
72
+ encodedRunMessages,
71
73
  options,
72
74
  );
73
75
  },
@@ -109,9 +111,9 @@ class AssistantCloudThreadHistoryAdapter implements ThreadHistoryAdapter {
109
111
  };
110
112
  }
111
113
 
112
- private _reportBatchTelemetry<T>(
114
+ private _reportRunTelemetry<T>(
113
115
  format: string,
114
- contents: T[],
116
+ runMessages: T[],
115
117
  options?: {
116
118
  durationMs?: number;
117
119
  stepTimestamps?: StepTimestamp[];
@@ -122,7 +124,7 @@ class AssistantCloudThreadHistoryAdapter implements ThreadHistoryAdapter {
122
124
  const remoteId = this.aui.threadListItem().getState().remoteId;
123
125
  if (!remoteId) return;
124
126
 
125
- const extracted = extractBatchTelemetry(format, contents);
127
+ const extracted = extractRunTelemetry(format, runMessages);
126
128
  if (!extracted) return;
127
129
 
128
130
  this._sendReport(
@@ -147,6 +149,8 @@ class AssistantCloudThreadHistoryAdapter implements ThreadHistoryAdapter {
147
149
  stepTimestamps?: StepTimestamp[],
148
150
  ) {
149
151
  const mergedSteps = mergeStepTimestamps(data.steps, stepTimestamps);
152
+ // Keep in sync with assistant-cloud createRunSchema
153
+ // (apps/aui-cloud-api/src/endpoints/runs/create.ts).
150
154
  const initial: Parameters<typeof this.cloudRef.current.runs.report>[0] = {
151
155
  thread_id: remoteId,
152
156
  status: data.status,
@@ -161,6 +165,12 @@ class AssistantCloudThreadHistoryAdapter implements ThreadHistoryAdapter {
161
165
  ...(data.outputTokens != null
162
166
  ? { output_tokens: data.outputTokens }
163
167
  : undefined),
168
+ ...(data.reasoningTokens != null
169
+ ? { reasoning_tokens: data.reasoningTokens }
170
+ : undefined),
171
+ ...(data.cachedInputTokens != null
172
+ ? { cached_input_tokens: data.cachedInputTokens }
173
+ : undefined),
164
174
  ...(durationMs != null ? { duration_ms: durationMs } : undefined),
165
175
  ...(data.outputText != null
166
176
  ? { output_text: data.outputText }
@@ -253,6 +263,8 @@ function buildToolCall(
253
263
  type TelemetryStepData = {
254
264
  input_tokens?: number;
255
265
  output_tokens?: number;
266
+ reasoning_tokens?: number;
267
+ cached_input_tokens?: number;
256
268
  tool_calls?: TelemetryToolCall[];
257
269
  start_ms?: number;
258
270
  end_ms?: number;
@@ -280,6 +292,8 @@ type TelemetryData = {
280
292
  totalSteps?: number;
281
293
  inputTokens?: number;
282
294
  outputTokens?: number;
295
+ reasoningTokens?: number;
296
+ cachedInputTokens?: number;
283
297
  outputText?: string;
284
298
  metadata?: Record<string, unknown>;
285
299
  steps?: TelemetryStepData[];
@@ -297,15 +311,15 @@ function extractTelemetry<T>(format: string, content: T): TelemetryData | null {
297
311
  }
298
312
  }
299
313
 
300
- function extractBatchTelemetry<T>(
314
+ function extractRunTelemetry<T>(
301
315
  format: string,
302
- contents: T[],
316
+ runMessages: T[],
303
317
  ): TelemetryData | null {
304
318
  if (format === "ai-sdk/v6") {
305
- return extractAiSdkV6Batch(contents);
319
+ return aggregateAiSdkV6RunSteps(runMessages);
306
320
  }
307
- for (let i = contents.length - 1; i >= 0; i--) {
308
- const result = extractTelemetry(format, contents[i]!);
321
+ for (let i = runMessages.length - 1; i >= 0; i--) {
322
+ const result = extractTelemetry(format, runMessages[i]!);
309
323
  if (result) return result;
310
324
  }
311
325
  return null;
@@ -332,7 +346,12 @@ function extractAuiV0<T>(content: T): TelemetryData | null {
332
346
  metadata?: {
333
347
  modelId?: string;
334
348
  steps?: readonly {
335
- usage?: { inputTokens?: number; outputTokens?: number };
349
+ usage?: {
350
+ inputTokens?: number;
351
+ outputTokens?: number;
352
+ reasoningTokens?: number;
353
+ cachedInputTokens?: number;
354
+ };
336
355
  }[];
337
356
  custom?: Record<string, unknown> & { modelId?: string };
338
357
  };
@@ -355,13 +374,39 @@ function extractAuiV0<T>(content: T): TelemetryData | null {
355
374
  const steps = msg.metadata?.steps;
356
375
  let inputTokens: number | undefined;
357
376
  let outputTokens: number | undefined;
377
+ let reasoningTokens: number | undefined;
378
+ let cachedInputTokens: number | undefined;
358
379
  if (steps && steps.length > 0) {
359
- inputTokens = 0;
360
- outputTokens = 0;
380
+ let totalInput = 0;
381
+ let totalOutput = 0;
382
+ let totalReasoning = 0;
383
+ let totalCachedInput = 0;
384
+ let hasInput = false;
385
+ let hasOutput = false;
386
+ let hasReasoning = false;
387
+ let hasCachedInput = false;
361
388
  for (const step of steps) {
362
- inputTokens += step.usage?.inputTokens ?? 0;
363
- outputTokens += step.usage?.outputTokens ?? 0;
389
+ if (step.usage?.inputTokens != null) {
390
+ totalInput += step.usage.inputTokens;
391
+ hasInput = true;
392
+ }
393
+ if (step.usage?.outputTokens != null) {
394
+ totalOutput += step.usage.outputTokens;
395
+ hasOutput = true;
396
+ }
397
+ if (step.usage?.reasoningTokens != null) {
398
+ totalReasoning += step.usage.reasoningTokens;
399
+ hasReasoning = true;
400
+ }
401
+ if (step.usage?.cachedInputTokens != null) {
402
+ totalCachedInput += step.usage.cachedInputTokens;
403
+ hasCachedInput = true;
404
+ }
364
405
  }
406
+ inputTokens = hasInput ? totalInput : undefined;
407
+ outputTokens = hasOutput ? totalOutput : undefined;
408
+ reasoningTokens = hasReasoning ? totalReasoning : undefined;
409
+ cachedInputTokens = hasCachedInput ? totalCachedInput : undefined;
365
410
  }
366
411
 
367
412
  const statusType = msg.status?.type;
@@ -384,6 +429,12 @@ function extractAuiV0<T>(content: T): TelemetryData | null {
384
429
  ...(s.usage?.outputTokens != null
385
430
  ? { output_tokens: s.usage.outputTokens }
386
431
  : undefined),
432
+ ...(s.usage?.reasoningTokens != null
433
+ ? { reasoning_tokens: s.usage.reasoningTokens }
434
+ : undefined),
435
+ ...(s.usage?.cachedInputTokens != null
436
+ ? { cached_input_tokens: s.usage.cachedInputTokens }
437
+ : undefined),
387
438
  }))
388
439
  : undefined;
389
440
 
@@ -393,6 +444,8 @@ function extractAuiV0<T>(content: T): TelemetryData | null {
393
444
  ...(steps?.length ? { totalSteps: steps.length } : undefined),
394
445
  ...(inputTokens != null ? { inputTokens } : undefined),
395
446
  ...(outputTokens != null ? { outputTokens } : undefined),
447
+ ...(reasoningTokens != null ? { reasoningTokens } : undefined),
448
+ ...(cachedInputTokens != null ? { cachedInputTokens } : undefined),
396
449
  ...(outputText != null ? { outputText } : undefined),
397
450
  ...(metadata ? { metadata } : undefined),
398
451
  ...(telemetrySteps ? { steps: telemetrySteps } : undefined),
@@ -491,7 +544,12 @@ function buildAiSdkV6Result(
491
544
  totalSteps: number,
492
545
  metadata?: Record<string, unknown>,
493
546
  stepsData?: { tool_calls: TelemetryToolCall[] }[],
494
- usage?: { inputTokens?: number; outputTokens?: number },
547
+ usage?: {
548
+ inputTokens?: number;
549
+ outputTokens?: number;
550
+ reasoningTokens?: number;
551
+ cachedInputTokens?: number;
552
+ },
495
553
  ): TelemetryData {
496
554
  const hasText = textParts.length > 0;
497
555
  const outputText = hasText ? truncateStr(textParts.join("")) : undefined;
@@ -516,6 +574,12 @@ function buildAiSdkV6Result(
516
574
  ...(usage?.outputTokens != null
517
575
  ? { outputTokens: usage.outputTokens }
518
576
  : undefined),
577
+ ...(usage?.reasoningTokens != null
578
+ ? { reasoningTokens: usage.reasoningTokens }
579
+ : undefined),
580
+ ...(usage?.cachedInputTokens != null
581
+ ? { cachedInputTokens: usage.cachedInputTokens }
582
+ : undefined),
519
583
  ...(outputText != null ? { outputText } : undefined),
520
584
  ...(metadata ? { metadata } : undefined),
521
585
  ...(steps ? { steps } : undefined),
@@ -528,23 +592,49 @@ type UsageFields = {
528
592
  outputTokens?: number;
529
593
  promptTokens?: number;
530
594
  completionTokens?: number;
595
+ reasoningTokens?: number;
596
+ cachedInputTokens?: number;
531
597
  };
532
598
 
533
- function normalizeUsage(
534
- u: UsageFields,
535
- ): { inputTokens: number; outputTokens: number } | undefined {
599
+ function normalizeUsage(u: UsageFields):
600
+ | {
601
+ inputTokens?: number;
602
+ outputTokens?: number;
603
+ reasoningTokens?: number;
604
+ cachedInputTokens?: number;
605
+ }
606
+ | undefined {
536
607
  const input = u.inputTokens ?? u.promptTokens;
537
608
  const output = u.outputTokens ?? u.completionTokens;
538
- if (input == null && output == null) return undefined;
609
+ if (
610
+ input == null &&
611
+ output == null &&
612
+ u.reasoningTokens == null &&
613
+ u.cachedInputTokens == null
614
+ ) {
615
+ return undefined;
616
+ }
617
+
539
618
  return {
540
- inputTokens: input ?? 0,
541
- outputTokens: output ?? 0,
619
+ ...(input != null ? { inputTokens: input } : undefined),
620
+ ...(output != null ? { outputTokens: output } : undefined),
621
+ ...(u.reasoningTokens != null
622
+ ? { reasoningTokens: u.reasoningTokens }
623
+ : undefined),
624
+ ...(u.cachedInputTokens != null
625
+ ? { cachedInputTokens: u.cachedInputTokens }
626
+ : undefined),
542
627
  };
543
628
  }
544
629
 
545
- function extractAiSdkV6Usage(
546
- metadata?: Record<string, unknown>,
547
- ): { inputTokens?: number; outputTokens?: number } | undefined {
630
+ function extractAiSdkV6Usage(metadata?: Record<string, unknown>):
631
+ | {
632
+ inputTokens?: number;
633
+ outputTokens?: number;
634
+ reasoningTokens?: number;
635
+ cachedInputTokens?: number;
636
+ }
637
+ | undefined {
548
638
  // Try top-level metadata.usage
549
639
  const usage = metadata?.usage as UsageFields | undefined;
550
640
  if (usage) {
@@ -559,17 +649,44 @@ function extractAiSdkV6Usage(
559
649
  if (steps && steps.length > 0) {
560
650
  let inputTokens = 0;
561
651
  let outputTokens = 0;
652
+ let reasoningTokens = 0;
653
+ let cachedInputTokens = 0;
654
+ let hasInput = false;
655
+ let hasOutput = false;
656
+ let hasReasoning = false;
657
+ let hasCachedInput = false;
562
658
  let hasAny = false;
563
659
  for (const s of steps) {
564
660
  if (!s.usage) continue;
565
661
  const n = normalizeUsage(s.usage);
566
662
  if (n) {
567
- inputTokens += n.inputTokens;
568
- outputTokens += n.outputTokens;
663
+ if (n.inputTokens != null) {
664
+ inputTokens += n.inputTokens;
665
+ hasInput = true;
666
+ }
667
+ if (n.outputTokens != null) {
668
+ outputTokens += n.outputTokens;
669
+ hasOutput = true;
670
+ }
671
+ if (n.reasoningTokens != null) {
672
+ reasoningTokens += n.reasoningTokens;
673
+ hasReasoning = true;
674
+ }
675
+ if (n.cachedInputTokens != null) {
676
+ cachedInputTokens += n.cachedInputTokens;
677
+ hasCachedInput = true;
678
+ }
569
679
  hasAny = true;
570
680
  }
571
681
  }
572
- if (hasAny) return { inputTokens, outputTokens };
682
+ if (hasAny) {
683
+ return {
684
+ ...(hasInput ? { inputTokens } : undefined),
685
+ ...(hasOutput ? { outputTokens } : undefined),
686
+ ...(hasReasoning ? { reasoningTokens } : undefined),
687
+ ...(hasCachedInput ? { cachedInputTokens } : undefined),
688
+ };
689
+ }
573
690
  }
574
691
 
575
692
  return undefined;
@@ -592,17 +709,22 @@ function extractAiSdkV6<T>(content: T): TelemetryData | null {
592
709
  );
593
710
  }
594
711
 
595
- function extractAiSdkV6Batch<T>(contents: T[]): TelemetryData | null {
712
+ function aggregateAiSdkV6RunSteps<T>(stepMessages: T[]): TelemetryData | null {
596
713
  const allTextParts: string[] = [];
597
714
  const allToolCalls: TelemetryToolCall[] = [];
598
715
  const allStepsData: { tool_calls: TelemetryToolCall[] }[] = [];
599
716
  let hasAssistant = false;
600
717
  let metadata: Record<string, unknown> | undefined;
601
- let aggregatedUsage:
602
- | { inputTokens: number; outputTokens: number }
603
- | undefined;
604
-
605
- for (const content of contents) {
718
+ let inputTokens = 0;
719
+ let outputTokens = 0;
720
+ let reasoningTokens = 0;
721
+ let cachedInputTokens = 0;
722
+ let hasInput = false;
723
+ let hasOutput = false;
724
+ let hasReasoning = false;
725
+ let hasCachedInput = false;
726
+
727
+ for (const content of stepMessages) {
606
728
  const msg = content as AiSdkV6Message;
607
729
  if (msg.role !== "assistant") continue;
608
730
  hasAssistant = true;
@@ -617,10 +739,22 @@ function extractAiSdkV6Batch<T>(contents: T[]): TelemetryData | null {
617
739
 
618
740
  const usage = extractAiSdkV6Usage(msg.metadata);
619
741
  if (usage) {
620
- if (!aggregatedUsage)
621
- aggregatedUsage = { inputTokens: 0, outputTokens: 0 };
622
- aggregatedUsage.inputTokens += usage.inputTokens ?? 0;
623
- aggregatedUsage.outputTokens += usage.outputTokens ?? 0;
742
+ if (usage.inputTokens != null) {
743
+ inputTokens += usage.inputTokens;
744
+ hasInput = true;
745
+ }
746
+ if (usage.outputTokens != null) {
747
+ outputTokens += usage.outputTokens;
748
+ hasOutput = true;
749
+ }
750
+ if (usage.reasoningTokens != null) {
751
+ reasoningTokens += usage.reasoningTokens;
752
+ hasReasoning = true;
753
+ }
754
+ if (usage.cachedInputTokens != null) {
755
+ cachedInputTokens += usage.cachedInputTokens;
756
+ hasCachedInput = true;
757
+ }
624
758
  }
625
759
  }
626
760
 
@@ -631,7 +765,12 @@ function extractAiSdkV6Batch<T>(contents: T[]): TelemetryData | null {
631
765
  allStepsData.length,
632
766
  metadata,
633
767
  allStepsData,
634
- aggregatedUsage,
768
+ {
769
+ ...(hasInput ? { inputTokens } : undefined),
770
+ ...(hasOutput ? { outputTokens } : undefined),
771
+ ...(hasReasoning ? { reasoningTokens } : undefined),
772
+ ...(hasCachedInput ? { cachedInputTokens } : undefined),
773
+ },
635
774
  );
636
775
  }
637
776
 
@@ -4,19 +4,22 @@ import type { ThreadAssistantMessage } from "@assistant-ui/core";
4
4
  import type { Tool } from "assistant-stream";
5
5
  import { act, renderHook, waitFor } from "@testing-library/react";
6
6
  import { describe, expect, it, vi } from "vitest";
7
- import type { AssistantTransportState, ToolExecutionStatus } from "./types";
8
- import { useToolInvocations } from "./useToolInvocations";
7
+ import type { AssistantTransportState } from "./types";
8
+ import { ToolExecutionStatus, useToolInvocations } from "./useToolInvocations";
9
+ import { ReadonlyJSONObject, ReadonlyJSONValue } from "assistant-stream/utils";
9
10
 
10
11
  const createState = (
11
12
  messages: ThreadAssistantMessage[],
13
+ isRunning: boolean = true,
12
14
  ): AssistantTransportState => ({
13
15
  messages,
14
- isRunning: true,
16
+ isRunning,
15
17
  });
16
18
 
17
19
  const createAssistantMessage = (
18
20
  argsText: string,
19
21
  args: Record<string, unknown>,
22
+ options?: { result?: ReadonlyJSONValue; isError?: boolean },
20
23
  ): ThreadAssistantMessage => ({
21
24
  id: "m-1",
22
25
  role: "assistant",
@@ -34,8 +37,10 @@ const createAssistantMessage = (
34
37
  type: "tool-call",
35
38
  toolCallId: "tool-1",
36
39
  toolName: "weatherSearch",
37
- args,
40
+ args: args as ReadonlyJSONObject,
38
41
  argsText,
42
+ ...(options?.result !== undefined && { result: options.result }),
43
+ ...(options?.isError !== undefined && { isError: options.isError }),
39
44
  },
40
45
  ],
41
46
  });
@@ -45,6 +50,7 @@ describe("useToolInvocations", () => {
45
50
  const execute = vi.fn(async () => ({ forecast: "ok" }));
46
51
  const getTools = () => ({
47
52
  weatherSearch: {
53
+ parameters: { type: "object", properties: {} },
48
54
  execute,
49
55
  } satisfies Tool,
50
56
  });
@@ -137,6 +143,7 @@ describe("useToolInvocations", () => {
137
143
  );
138
144
  const getTools = () => ({
139
145
  weatherSearch: {
146
+ parameters: { type: "object", properties: {} },
140
147
  execute,
141
148
  } satisfies Tool,
142
149
  });
@@ -220,4 +227,223 @@ describe("useToolInvocations", () => {
220
227
  });
221
228
  expect(Object.keys(statuses)).not.toContain("tool-1:rewrite:0");
222
229
  });
230
+
231
+ it("does not close args stream early for non-executable tool snapshots", () => {
232
+ const getTools = () => ({
233
+ weatherSearch: {
234
+ parameters: { type: "object", properties: {} },
235
+ } satisfies Tool,
236
+ });
237
+ const onResult = vi.fn();
238
+ const setToolStatuses = vi.fn();
239
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
240
+
241
+ try {
242
+ const { rerender } = renderHook(
243
+ ({ state }: { state: AssistantTransportState }) =>
244
+ useToolInvocations({
245
+ state,
246
+ getTools,
247
+ onResult,
248
+ setToolStatuses,
249
+ }),
250
+ {
251
+ initialProps: {
252
+ state: createState([]),
253
+ },
254
+ },
255
+ );
256
+
257
+ act(() => {
258
+ rerender({
259
+ state: createState([createAssistantMessage("{}", {})]),
260
+ });
261
+ });
262
+
263
+ act(() => {
264
+ rerender({
265
+ state: createState([
266
+ createAssistantMessage('{"title":"Weekly"', {
267
+ title: "Weekly",
268
+ }),
269
+ ]),
270
+ });
271
+ });
272
+
273
+ act(() => {
274
+ rerender({
275
+ state: createState([
276
+ createAssistantMessage('{"title":"Weekly","columns":["name"]}', {
277
+ title: "Weekly",
278
+ columns: ["name"],
279
+ }),
280
+ ]),
281
+ });
282
+ });
283
+
284
+ expect(warnSpy).not.toHaveBeenCalledWith(
285
+ "argsText updated after controller was closed:",
286
+ expect.anything(),
287
+ );
288
+ expect(warnSpy).not.toHaveBeenCalledWith(
289
+ "argsText updated after controller was closed, restarting tool args stream:",
290
+ expect.anything(),
291
+ );
292
+ expect(onResult).not.toHaveBeenCalled();
293
+ } finally {
294
+ warnSpy.mockRestore();
295
+ }
296
+ });
297
+
298
+ it("closes non-executable complete args stream after run settles", () => {
299
+ const getTools = () => ({
300
+ weatherSearch: {
301
+ parameters: { type: "object", properties: {} },
302
+ } satisfies Tool,
303
+ });
304
+ const onResult = vi.fn();
305
+ const setToolStatuses = vi.fn();
306
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
307
+
308
+ try {
309
+ const { rerender } = renderHook(
310
+ ({ state }: { state: AssistantTransportState }) =>
311
+ useToolInvocations({
312
+ state,
313
+ getTools,
314
+ onResult,
315
+ setToolStatuses,
316
+ }),
317
+ {
318
+ initialProps: {
319
+ state: createState([]),
320
+ },
321
+ },
322
+ );
323
+
324
+ act(() => {
325
+ rerender({
326
+ state: createState(
327
+ [
328
+ createAssistantMessage('{"title":"Weekly"}', {
329
+ title: "Weekly",
330
+ }),
331
+ ],
332
+ true,
333
+ ),
334
+ });
335
+ });
336
+
337
+ act(() => {
338
+ rerender({
339
+ state: createState(
340
+ [
341
+ createAssistantMessage('{"title":"Weekly"}', {
342
+ title: "Weekly",
343
+ }),
344
+ ],
345
+ false,
346
+ ),
347
+ });
348
+ });
349
+
350
+ act(() => {
351
+ rerender({
352
+ state: createState(
353
+ [
354
+ createAssistantMessage('{"title":"Weekly","columns":["name"]}', {
355
+ title: "Weekly",
356
+ columns: ["name"],
357
+ }),
358
+ ],
359
+ false,
360
+ ),
361
+ });
362
+ });
363
+
364
+ expect(warnSpy).toHaveBeenCalledWith(
365
+ "argsText updated after controller was closed, restarting tool args stream:",
366
+ expect.objectContaining({
367
+ previous: '{"title":"Weekly"}',
368
+ next: '{"title":"Weekly","columns":["name"]}',
369
+ }),
370
+ );
371
+ expect(onResult).not.toHaveBeenCalled();
372
+ } finally {
373
+ warnSpy.mockRestore();
374
+ }
375
+ });
376
+
377
+ it("handles backend result when equivalent complete argsText reorders keys", async () => {
378
+ let resolveExecute: ((value: unknown) => void) | undefined;
379
+ const execute = vi.fn(
380
+ () =>
381
+ new Promise<unknown>((resolve) => {
382
+ resolveExecute = resolve;
383
+ }),
384
+ );
385
+ const getTools = () => ({
386
+ weatherSearch: {
387
+ parameters: { type: "object", properties: {} },
388
+ execute,
389
+ } satisfies Tool,
390
+ });
391
+ const onResult = vi.fn();
392
+ const setToolStatuses = vi.fn();
393
+
394
+ const { rerender } = renderHook(
395
+ ({ state }: { state: AssistantTransportState }) =>
396
+ useToolInvocations({
397
+ state,
398
+ getTools,
399
+ onResult,
400
+ setToolStatuses,
401
+ }),
402
+ {
403
+ initialProps: {
404
+ state: createState([]),
405
+ },
406
+ },
407
+ );
408
+
409
+ act(() => {
410
+ rerender({
411
+ state: createState([
412
+ createAssistantMessage('{"a":1,"b":2}', {
413
+ a: 1,
414
+ b: 2,
415
+ }),
416
+ ]),
417
+ });
418
+ });
419
+
420
+ await waitFor(() => {
421
+ expect(execute).toHaveBeenCalledTimes(1);
422
+ });
423
+
424
+ act(() => {
425
+ rerender({
426
+ state: createState([
427
+ createAssistantMessage(
428
+ '{"b":2,"a":1}',
429
+ {
430
+ a: 1,
431
+ b: 2,
432
+ },
433
+ {
434
+ result: { source: "backend" },
435
+ },
436
+ ),
437
+ ]),
438
+ });
439
+ });
440
+
441
+ await act(async () => {
442
+ resolveExecute?.({ source: "client" });
443
+ });
444
+
445
+ await waitFor(() => {
446
+ expect(onResult).not.toHaveBeenCalled();
447
+ });
448
+ });
223
449
  });