@assistant-ui/ai-sdk 0.0.10 → 0.0.12

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 (124) hide show
  1. package/dist/converters/convertMessage.d.ts +8 -6
  2. package/dist/converters/convertMessage.d.ts.map +1 -1
  3. package/dist/converters/convertMessage.js +106 -28
  4. package/dist/converters/convertMessage.js.map +1 -1
  5. package/dist/converters/toolApprovalAnswers.d.ts +3 -0
  6. package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
  7. package/dist/converters/toolApprovalAnswers.js +17 -0
  8. package/dist/converters/toolApprovalAnswers.js.map +1 -0
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.native.d.ts +1 -1
  13. package/dist/index.native.d.ts.map +1 -1
  14. package/dist/index.native.js +1 -1
  15. package/dist/model-context/injectInteractableContext.d.ts +3 -0
  16. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  17. package/dist/model-context/injectInteractableContext.js +3 -0
  18. package/dist/model-context/injectInteractableContext.js.map +1 -1
  19. package/dist/model-context/injectQuoteContext.d.ts +1 -0
  20. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  21. package/dist/model-context/injectQuoteContext.js +1 -0
  22. package/dist/model-context/injectQuoteContext.js.map +1 -1
  23. package/dist/runtime/AISDKChat.js +1 -1
  24. package/dist/runtime/AISDKThreads.d.ts +5 -0
  25. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  26. package/dist/runtime/AISDKThreads.js +37 -4
  27. package/dist/runtime/AISDKThreads.js.map +1 -1
  28. package/dist/runtime/DynamicChatTransport.d.ts +49 -0
  29. package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
  30. package/dist/runtime/DynamicChatTransport.js +147 -0
  31. package/dist/runtime/DynamicChatTransport.js.map +1 -0
  32. package/dist/runtime/getResumableAdapter.d.ts +5 -0
  33. package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
  34. package/dist/runtime/getResumableAdapter.js +12 -0
  35. package/dist/runtime/getResumableAdapter.js.map +1 -0
  36. package/dist/runtime/sdkIdentity.js +1 -1
  37. package/dist/runtime/toolHistoryCodec.d.ts +20 -0
  38. package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
  39. package/dist/runtime/toolHistoryCodec.js +107 -0
  40. package/dist/runtime/toolHistoryCodec.js.map +1 -0
  41. package/dist/runtime/useAISDKRuntime.d.ts +4 -1
  42. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  43. package/dist/runtime/useAISDKRuntime.js +154 -155
  44. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  45. package/dist/runtime/useChatRuntime.d.ts +14 -2
  46. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  47. package/dist/runtime/useChatRuntime.js +12 -3
  48. package/dist/runtime/useChatRuntime.js.map +1 -1
  49. package/dist/runtime/useChatThread.d.ts +3 -2
  50. package/dist/runtime/useChatThread.d.ts.map +1 -1
  51. package/dist/runtime/useChatThread.js +56 -36
  52. package/dist/runtime/useChatThread.js.map +1 -1
  53. package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
  54. package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
  55. package/dist/runtime/useDynamicChatTransport.js +64 -0
  56. package/dist/runtime/useDynamicChatTransport.js.map +1 -0
  57. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  58. package/dist/runtime/useExternalHistory.js +11 -105
  59. package/dist/runtime/useExternalHistory.js.map +1 -1
  60. package/dist/runtime/useResourceCleanup.js +1 -1
  61. package/dist/runtime/useStreamingTiming.js +2 -2
  62. package/dist/runtime/useStreamingTiming.js.map +1 -1
  63. package/dist/tools/generativeTools.d.ts +2 -1
  64. package/dist/tools/generativeTools.d.ts.map +1 -1
  65. package/dist/tools/generativeTools.js +17 -6
  66. package/dist/tools/generativeTools.js.map +1 -1
  67. package/dist/usage.js +1 -1
  68. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  69. package/dist/utils/sliceMessagesUntil.js +1 -2
  70. package/dist/utils/sliceMessagesUntil.js.map +1 -1
  71. package/package.json +12 -10
  72. package/src/converters/convertMessage.test.ts +350 -2
  73. package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
  74. package/src/converters/convertMessage.ts +154 -23
  75. package/src/converters/toCreateMessage.test.ts +27 -0
  76. package/src/converters/toolApprovalAnswers.ts +27 -0
  77. package/src/index.native.ts +1 -1
  78. package/src/index.ts +1 -1
  79. package/src/model-context/injectInteractableContext.ts +3 -0
  80. package/src/model-context/injectQuoteContext.ts +1 -0
  81. package/src/runtime/AISDKChat.integration.test.tsx +57 -2
  82. package/src/runtime/AISDKThreads.cloud.test.ts +3 -0
  83. package/src/runtime/AISDKThreads.test.ts +181 -0
  84. package/src/runtime/AISDKThreads.ts +31 -4
  85. package/src/runtime/DynamicChatTransport.test.ts +203 -0
  86. package/src/runtime/DynamicChatTransport.ts +273 -0
  87. package/src/runtime/__tests__/controlled-transport.ts +3 -0
  88. package/src/runtime/getResumableAdapter.ts +16 -0
  89. package/src/runtime/toolHistoryCodec.test.ts +161 -0
  90. package/src/runtime/toolHistoryCodec.ts +207 -0
  91. package/src/runtime/useAISDKRuntime.approval.test.tsx +12 -0
  92. package/src/runtime/useAISDKRuntime.reload.test.tsx +219 -0
  93. package/src/runtime/useAISDKRuntime.test.ts +538 -5
  94. package/src/runtime/useAISDKRuntime.ts +161 -51
  95. package/src/runtime/useChatRuntime.integration.test.tsx +319 -4
  96. package/src/runtime/useChatRuntime.local-storage.test.tsx +123 -0
  97. package/src/runtime/useChatRuntime.test.ts +107 -1
  98. package/src/runtime/useChatRuntime.ts +26 -4
  99. package/src/runtime/useChatThread.binding.test.tsx +143 -0
  100. package/src/runtime/useChatThread.ts +98 -81
  101. package/src/runtime/useDynamicChatTransport.ts +26 -0
  102. package/src/runtime/useExternalHistory.test.ts +205 -0
  103. package/src/runtime/useExternalHistory.ts +14 -206
  104. package/src/runtime/useStreamingTiming.ts +2 -2
  105. package/src/tools/generativeTools.test.ts +190 -2
  106. package/src/tools/generativeTools.ts +28 -8
  107. package/src/utils/sliceMessagesUntil.test.ts +2 -6
  108. package/src/utils/sliceMessagesUntil.ts +1 -5
  109. package/dist/converters/modelContentEnvelope.d.ts +0 -14
  110. package/dist/converters/modelContentEnvelope.d.ts.map +0 -1
  111. package/dist/converters/modelContentEnvelope.js +0 -22
  112. package/dist/converters/modelContentEnvelope.js.map +0 -1
  113. package/dist/converters/toolOutputConversion.d.ts +0 -26
  114. package/dist/converters/toolOutputConversion.d.ts.map +0 -1
  115. package/dist/converters/toolOutputConversion.js +0 -31
  116. package/dist/converters/toolOutputConversion.js.map +0 -1
  117. package/dist/tools/frontendTools.d.ts +0 -30
  118. package/dist/tools/frontendTools.d.ts.map +0 -1
  119. package/dist/tools/frontendTools.js +0 -33
  120. package/dist/tools/frontendTools.js.map +0 -1
  121. package/src/converters/modelContentEnvelope.ts +0 -41
  122. package/src/converters/toolOutputConversion.ts +0 -26
  123. package/src/tools/frontendTools.test.ts +0 -205
  124. package/src/tools/frontendTools.ts +0 -83
@@ -666,6 +666,119 @@ describe("AISDKMessageConverter", () => {
666
666
  ]);
667
667
  });
668
668
 
669
+ it("reads questions and recorded answers from the approval descriptor for a custom response channel", () => {
670
+ const questions = [
671
+ {
672
+ id: "scope",
673
+ prompt: "Which files?",
674
+ header: "Scope",
675
+ options: [
676
+ { id: "src", label: "src", description: "Sources" },
677
+ { id: 1, label: "invalid" },
678
+ ],
679
+ multiple: true,
680
+ },
681
+ { id: "note", prompt: "Anything else?", allowFreeform: "yes" },
682
+ { prompt: "No id" },
683
+ ];
684
+ const parts = [
685
+ {
686
+ type: "tool-ask",
687
+ toolCallId: "tc-1",
688
+ state: "approval-requested",
689
+ input: {},
690
+ approval: {
691
+ id: "approval-1",
692
+ descriptor: { display: "questions", questions },
693
+ },
694
+ },
695
+ {
696
+ type: "tool-ask",
697
+ toolCallId: "tc-2",
698
+ state: "approval-responded",
699
+ input: {},
700
+ approval: {
701
+ id: "approval-2",
702
+ approved: true,
703
+ descriptor: {
704
+ display: "questions",
705
+ questions: [{ id: "note", prompt: "Anything else?" }],
706
+ answers: {
707
+ note: { text: "no", optionIds: [1, "x"] },
708
+ bad: "x",
709
+ empty: { optionIds: [1] },
710
+ },
711
+ },
712
+ },
713
+ },
714
+ ];
715
+
716
+ const convert = (supportsRichToolApprovalResponses: boolean) => {
717
+ const metadata: AISDKMessageConverterMetadata = {
718
+ supportsRichToolApprovalResponses,
719
+ };
720
+ return AISDKMessageConverter.toThreadMessages(
721
+ [{ id: "a1", role: "assistant", parts } as any],
722
+ false,
723
+ metadata,
724
+ )[0]?.content.map((part) => (part as { approval?: any }).approval);
725
+ };
726
+
727
+ const [requested, responded] = convert(true)!;
728
+ expect(requested.display).toBe("questions");
729
+ expect(requested.questions).toEqual([
730
+ {
731
+ id: "scope",
732
+ prompt: "Which files?",
733
+ header: "Scope",
734
+ options: [{ id: "src", label: "src", description: "Sources" }],
735
+ multiple: true,
736
+ },
737
+ { id: "note", prompt: "Anything else?" },
738
+ ]);
739
+ expect(responded.approved).toBe(true);
740
+ expect(responded.answers).toEqual({
741
+ note: { text: "no", optionIds: ["x"] },
742
+ });
743
+ expect(
744
+ convertWith(JSON.parse('{"__proto__":{"text":"kept"}}')).answers
745
+ .__proto__,
746
+ ).toEqual({ text: "kept" });
747
+
748
+ function convertWith(answers: unknown) {
749
+ const metadata: AISDKMessageConverterMetadata = {
750
+ supportsRichToolApprovalResponses: true,
751
+ };
752
+ return AISDKMessageConverter.toThreadMessages(
753
+ [
754
+ {
755
+ id: "a2",
756
+ role: "assistant",
757
+ parts: [
758
+ {
759
+ type: "tool-ask",
760
+ toolCallId: "tc-3",
761
+ state: "approval-responded",
762
+ input: {},
763
+ approval: {
764
+ id: "approval-3",
765
+ approved: true,
766
+ descriptor: { display: "questions", answers },
767
+ },
768
+ },
769
+ ],
770
+ } as any,
771
+ ],
772
+ false,
773
+ metadata,
774
+ )[0]?.content.map((part) => (part as { approval?: any }).approval)[0];
775
+ }
776
+
777
+ const [builtIn] = convert(false)!;
778
+ expect(builtIn).not.toHaveProperty("display");
779
+ expect(builtIn).not.toHaveProperty("questions");
780
+ });
781
+
669
782
  it("ignores a non-boolean dismissible field from the approval descriptor", () => {
670
783
  const metadata: AISDKMessageConverterMetadata = {
671
784
  supportsRichToolApprovalResponses: true,
@@ -1277,7 +1390,9 @@ describe("AISDKMessageConverter", () => {
1277
1390
  const call = terminal[0]?.content.find(
1278
1391
  (part): part is any => part.type === "tool-call",
1279
1392
  );
1280
- expect(call?.args).toEqual({ city: "NYC" });
1393
+ expect(Object.fromEntries(Object.entries(call?.args ?? {}))).toEqual({
1394
+ city: "NYC",
1395
+ });
1281
1396
  expect(call?.result).toEqual({ temp: 70 });
1282
1397
  });
1283
1398
 
@@ -1416,6 +1531,123 @@ describe("AISDKMessageConverter", () => {
1416
1531
  });
1417
1532
  });
1418
1533
 
1534
+ it("forwards toolMetadata.app without a tool-supplied serverId", () => {
1535
+ const converted = AISDKMessageConverter.toThreadMessages([
1536
+ {
1537
+ id: "a1",
1538
+ role: "assistant",
1539
+ parts: [
1540
+ {
1541
+ type: "tool-search",
1542
+ toolCallId: "tc-1",
1543
+ state: "output-available",
1544
+ input: { query: "hi" },
1545
+ output: { results: [] },
1546
+ toolMetadata: {
1547
+ app: {
1548
+ resourceUri: "ui://example/search",
1549
+ mimeType: "text/html;profile=mcp-app",
1550
+ visibility: ["app", "model", "bogus"],
1551
+ serverId: "search-server",
1552
+ },
1553
+ },
1554
+ },
1555
+ ],
1556
+ } as any,
1557
+ ]);
1558
+
1559
+ const call = converted[0]?.content.find(
1560
+ (part): part is any => part.type === "tool-call",
1561
+ );
1562
+ expect(call?.mcp?.app).toEqual({
1563
+ resourceUri: "ui://example/search",
1564
+ mimeType: "text/html;profile=mcp-app",
1565
+ visibility: ["app", "model"],
1566
+ });
1567
+ });
1568
+
1569
+ it.each([undefined, "", "other-server"])(
1570
+ "keeps provider serverId when the tool supplies %s",
1571
+ (serverId) => {
1572
+ const converted = AISDKMessageConverter.toThreadMessages([
1573
+ {
1574
+ id: "a1",
1575
+ role: "assistant",
1576
+ parts: [
1577
+ {
1578
+ type: "tool-search",
1579
+ toolCallId: "tc-1",
1580
+ state: "output-available",
1581
+ input: { query: "hi" },
1582
+ output: { results: [] },
1583
+ toolMetadata: {
1584
+ app: {
1585
+ resourceUri: "ui://example/search",
1586
+ mimeType: "text/html;profile=mcp-app",
1587
+ serverId,
1588
+ },
1589
+ },
1590
+ callProviderMetadata: {
1591
+ mcp: {
1592
+ app: {
1593
+ resourceUri: "ui://example/search",
1594
+ serverId: "search-server",
1595
+ },
1596
+ },
1597
+ },
1598
+ },
1599
+ ],
1600
+ } as any,
1601
+ ]);
1602
+
1603
+ const call = converted[0]?.content.find(
1604
+ (part): part is any => part.type === "tool-call",
1605
+ );
1606
+ expect(call?.mcp?.app).toEqual({
1607
+ resourceUri: "ui://example/search",
1608
+ mimeType: "text/html;profile=mcp-app",
1609
+ serverId: "search-server",
1610
+ });
1611
+ },
1612
+ );
1613
+
1614
+ it("falls back to provider app metadata when toolMetadata.app has an invalid URI", () => {
1615
+ const converted = AISDKMessageConverter.toThreadMessages([
1616
+ {
1617
+ id: "a1",
1618
+ role: "assistant",
1619
+ parts: [
1620
+ {
1621
+ type: "tool-search",
1622
+ toolCallId: "tc-1",
1623
+ state: "output-available",
1624
+ input: { query: "hi" },
1625
+ output: { results: [] },
1626
+ toolMetadata: {
1627
+ app: { resourceUri: "https://example.com/search" },
1628
+ },
1629
+ callProviderMetadata: {
1630
+ mcp: {
1631
+ app: {
1632
+ resourceUri: "ui://example/search",
1633
+ mimeType: "text/html;profile=mcp-app",
1634
+ },
1635
+ },
1636
+ },
1637
+ },
1638
+ ],
1639
+ } as any,
1640
+ ]);
1641
+
1642
+ const call = converted[0]?.content.find(
1643
+ (part): part is any => part.type === "tool-call",
1644
+ );
1645
+ expect(call?.mcp?.app).toEqual({
1646
+ resourceUri: "ui://example/search",
1647
+ mimeType: "text/html;profile=mcp-app",
1648
+ });
1649
+ });
1650
+
1419
1651
  it("omits an empty callProviderMetadata.mcp.app.serverId", () => {
1420
1652
  const converted = AISDKMessageConverter.toThreadMessages([
1421
1653
  {
@@ -1864,7 +2096,10 @@ describe("AISDKMessageConverter", () => {
1864
2096
  const toolCall = converted[0]?.content.find(
1865
2097
  (part): part is any => part.type === "tool-call",
1866
2098
  );
1867
- expect(toolCall?.args).toEqual({ city: "NYC", units: "F" });
2099
+ expect(Object.fromEntries(Object.entries(toolCall?.args ?? {}))).toEqual({
2100
+ city: "NYC",
2101
+ units: "F",
2102
+ });
1868
2103
  expect(toolCall?.argsText).toBe('{"city":"NYC","units":"F"}');
1869
2104
  });
1870
2105
 
@@ -1938,6 +2173,7 @@ describe("AISDKMessageConverter", () => {
1938
2173
 
1939
2174
  expect(a.argsText).toBe('{"a":1,"b":2}');
1940
2175
  expect(b.argsText).toBe('{"b":2,"a":1}');
2176
+ expect(Object.keys(b.args)).toEqual(["a", "b"]);
1941
2177
 
1942
2178
  // Both entries must survive a reconversion: the key-order entries are gone
1943
2179
  // by now, so a cache miss would re-serialize B in raw key order.
@@ -1977,4 +2213,116 @@ describe("AISDKMessageConverter", () => {
1977
2213
  }
1978
2214
  expect(stableStringifySpy).toHaveBeenCalledTimes(1);
1979
2215
  });
2216
+
2217
+ it("skips null parts and parts without a type", () => {
2218
+ const converted = AISDKMessageConverter.toThreadMessages([
2219
+ {
2220
+ id: "u1",
2221
+ role: "user",
2222
+ parts: [null, { text: "no type" }, { type: "text", text: "hi" }],
2223
+ },
2224
+ {
2225
+ id: "a1",
2226
+ role: "assistant",
2227
+ parts: [null, { text: "no type" }, { type: "text", text: "yo" }],
2228
+ },
2229
+ ] as any);
2230
+
2231
+ expect(converted[0]?.content).toMatchObject([{ type: "text", text: "hi" }]);
2232
+ expect(converted[0]?.attachments).toEqual([]);
2233
+ expect(converted[1]?.content).toMatchObject([{ type: "text", text: "yo" }]);
2234
+ });
2235
+
2236
+ it("joins the text parts of a system message into one text part", () => {
2237
+ const converted = AISDKMessageConverter.toThreadMessages([
2238
+ {
2239
+ id: "s1",
2240
+ role: "system",
2241
+ parts: [
2242
+ {
2243
+ type: "text",
2244
+ text: "be ",
2245
+ providerMetadata: { p1: { k: "a" }, p2: { k: "b" } },
2246
+ },
2247
+ { type: "text", text: "brief", providerMetadata: { p2: { k: "c" } } },
2248
+ ],
2249
+ },
2250
+ { id: "s2", role: "system", parts: [] },
2251
+ ] as any);
2252
+
2253
+ expect(converted[0]?.content).toMatchObject([
2254
+ {
2255
+ type: "text",
2256
+ text: "be brief",
2257
+ providerMetadata: { p1: { k: "a" }, p2: { k: "c" } },
2258
+ },
2259
+ ]);
2260
+ expect(converted[1]?.content).toMatchObject([{ type: "text", text: "" }]);
2261
+ expect(converted[1]?.content[0]).not.toHaveProperty("providerMetadata");
2262
+ });
2263
+
2264
+ it("reads a user text part without text as empty text", () => {
2265
+ const converted = AISDKMessageConverter.toThreadMessages([
2266
+ { id: "u1", role: "user", parts: [{ type: "text" }] },
2267
+ ] as any);
2268
+
2269
+ expect(converted[0]?.content).toMatchObject([{ type: "text", text: "" }]);
2270
+ });
2271
+
2272
+ it("skips a file part without a url and floors a missing mediaType", () => {
2273
+ const converted = AISDKMessageConverter.toThreadMessages([
2274
+ {
2275
+ id: "u1",
2276
+ role: "user",
2277
+ parts: [{ type: "file", mediaType: "image/png", filename: "a.png" }],
2278
+ },
2279
+ {
2280
+ id: "a1",
2281
+ role: "assistant",
2282
+ parts: [
2283
+ { type: "file", mediaType: "image/png" },
2284
+ { type: "file", url: "https://cdn/file.bin" },
2285
+ { type: "reasoning-file", mediaType: "image/png" },
2286
+ { type: "reasoning-file", url: "https://cdn/thought.bin" },
2287
+ ],
2288
+ },
2289
+ ] as any);
2290
+
2291
+ expect(converted[0]?.attachments).toEqual([]);
2292
+ expect(converted[1]?.content).toMatchObject([
2293
+ {
2294
+ type: "file",
2295
+ data: "https://cdn/file.bin",
2296
+ mimeType: "unknown/unknown",
2297
+ },
2298
+ {
2299
+ type: "file",
2300
+ data: "https://cdn/thought.bin",
2301
+ mimeType: "unknown/unknown",
2302
+ },
2303
+ ]);
2304
+ });
2305
+
2306
+ it("gives a dynamic tool call without a toolName an empty name", () => {
2307
+ const converted = AISDKMessageConverter.toThreadMessages([
2308
+ {
2309
+ id: "a1",
2310
+ role: "assistant",
2311
+ parts: [
2312
+ {
2313
+ type: "dynamic-tool",
2314
+ toolCallId: "tc-1",
2315
+ state: "input-available",
2316
+ input: {},
2317
+ },
2318
+ ],
2319
+ },
2320
+ ] as any);
2321
+
2322
+ expect(converted[0]?.content[0]).toMatchObject({
2323
+ type: "tool-call",
2324
+ toolCallId: "tc-1",
2325
+ toolName: "",
2326
+ });
2327
+ });
1980
2328
  });
@@ -0,0 +1,185 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ getPartialJsonObjectFieldState,
4
+ getPartialJsonObjectMeta,
5
+ } from "assistant-stream/utils";
6
+ import {
7
+ AISDKMessageConverter,
8
+ type AISDKMessageConverterMetadata,
9
+ } from "./convertMessage";
10
+
11
+ describe("AI SDK tool argument completion", () => {
12
+ it.each([{}, { city: "Paris", unit: "c" }])(
13
+ "marks input-available arguments complete before tool execution ends for %j",
14
+ (input) => {
15
+ const convert = (toolState: "input-streaming" | "input-available") => {
16
+ const message = AISDKMessageConverter.toThreadMessages(
17
+ [
18
+ {
19
+ id: "a1",
20
+ role: "assistant",
21
+ parts: [
22
+ {
23
+ type: "tool-weather",
24
+ toolCallId: "tc-1",
25
+ state: toolState,
26
+ input,
27
+ },
28
+ ],
29
+ },
30
+ ],
31
+ true,
32
+ )[0]!;
33
+ return message.content.find((part) => part.type === "tool-call")!;
34
+ };
35
+
36
+ const streaming = convert("input-streaming");
37
+ expect(getPartialJsonObjectMeta(streaming.args)?.state).toBe("partial");
38
+
39
+ const available = convert("input-available");
40
+ expect(Object.fromEntries(Object.entries(available.args))).toEqual(input);
41
+ expect(JSON.parse(available.argsText)).toEqual(input);
42
+ expect(getPartialJsonObjectMeta(available.args)?.state).toBe("complete");
43
+ for (const key of Object.keys(input)) {
44
+ expect(getPartialJsonObjectFieldState(available.args, [key])).toBe(
45
+ "complete",
46
+ );
47
+ }
48
+ },
49
+ );
50
+
51
+ it("keeps the streaming frontier partial until input becomes available", () => {
52
+ const message = AISDKMessageConverter.toThreadMessages(
53
+ [
54
+ {
55
+ id: "a1",
56
+ role: "assistant",
57
+ parts: [
58
+ {
59
+ type: "tool-weather",
60
+ toolCallId: "tc-1",
61
+ state: "input-streaming",
62
+ input: { city: "Paris", unit: "c" },
63
+ },
64
+ ],
65
+ },
66
+ ],
67
+ true,
68
+ )[0]!;
69
+ const part = message.content.find((item) => item.type === "tool-call")!;
70
+ expect(getPartialJsonObjectMeta(part.args)?.state).toBe("partial");
71
+ expect(getPartialJsonObjectFieldState(part.args, ["city"])).toBe(
72
+ "complete",
73
+ );
74
+ expect(getPartialJsonObjectFieldState(part.args, ["unit"])).toBe("partial");
75
+ });
76
+
77
+ it.each([
78
+ {
79
+ state: "approval-responded",
80
+ approval: { id: "approval-1", approved: true },
81
+ },
82
+ {
83
+ state: "output-available",
84
+ output: { progress: 50 },
85
+ preliminary: true,
86
+ },
87
+ ])("marks settled $state input complete while the part runs", (tool) => {
88
+ const message = AISDKMessageConverter.toThreadMessages(
89
+ [
90
+ {
91
+ id: "a1",
92
+ role: "assistant",
93
+ parts: [
94
+ {
95
+ type: "tool-weather",
96
+ toolCallId: "tc-1",
97
+ input: { city: "Paris" },
98
+ ...tool,
99
+ },
100
+ ],
101
+ },
102
+ ] as any,
103
+ true,
104
+ )[0]!;
105
+ const part = message.content.find((item) => item.type === "tool-call")!;
106
+ expect(message.status?.type).toBe("running");
107
+ expect(getPartialJsonObjectMeta(part.args)?.state).toBe("complete");
108
+ expect(getPartialJsonObjectFieldState(part.args, ["city"])).toBe(
109
+ "complete",
110
+ );
111
+ });
112
+
113
+ it("reuses marked arguments when the settled input object is unchanged", () => {
114
+ const input = { weather: { city: "Paris" } };
115
+ const metadata: AISDKMessageConverterMetadata = {
116
+ toolArgsTextCache: new WeakMap(),
117
+ };
118
+ const convert = () =>
119
+ AISDKMessageConverter.toThreadMessages(
120
+ [
121
+ {
122
+ id: "a1",
123
+ role: "assistant",
124
+ parts: [
125
+ {
126
+ type: "tool-weather",
127
+ toolCallId: "tc-1",
128
+ state: "input-available",
129
+ input,
130
+ },
131
+ ],
132
+ },
133
+ ],
134
+ true,
135
+ metadata,
136
+ )[0]!.content.find((part) => part.type === "tool-call")!;
137
+
138
+ const first = convert();
139
+ const second = convert();
140
+ expect(first.args).toBe(second.args);
141
+ expect(first.args).not.toBe(input);
142
+ expect(first.args.weather).toBe(input.weather);
143
+ expect(first.argsText).toBe(second.argsText);
144
+ expect(getPartialJsonObjectMeta(second.args)?.state).toBe("complete");
145
+ });
146
+
147
+ it.each([
148
+ '{"__proto__":{"polluted":true}}',
149
+ '{"constructor":{"prototype":{"polluted":true}}}',
150
+ ])(
151
+ "marks settled prototype-named input complete while the tool runs: %s",
152
+ (json) => {
153
+ const input = JSON.parse(json);
154
+ const message = AISDKMessageConverter.toThreadMessages(
155
+ [
156
+ {
157
+ id: "a1",
158
+ role: "assistant",
159
+ parts: [
160
+ {
161
+ type: "tool-weather",
162
+ toolCallId: "tc-1",
163
+ state: "input-available",
164
+ input,
165
+ },
166
+ ],
167
+ },
168
+ ],
169
+ true,
170
+ )[0]!;
171
+ const part = message.content.find((item) => item.type === "tool-call")!;
172
+
173
+ expect(message.status?.type).toBe("running");
174
+ expect(part.args).not.toBe(input);
175
+ expect(Object.entries(part.args)).toEqual(Object.entries(input));
176
+ expect(Object.getPrototypeOf(part.args)).toBe(Object.prototype);
177
+ expect(Object.prototype).not.toHaveProperty("polluted");
178
+ expect(JSON.parse(part.argsText)).toEqual(input);
179
+ expect(getPartialJsonObjectMeta(part.args)?.state).toBe("complete");
180
+ expect(
181
+ getPartialJsonObjectFieldState(part.args, [Object.keys(input)[0]!]),
182
+ ).toBe("complete");
183
+ },
184
+ );
185
+ });