@assistant-ui/ai-sdk 0.0.8 → 0.0.10

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 (51) hide show
  1. package/LICENSE +1 -1
  2. package/dist/converters/convertMessage.d.ts +2 -0
  3. package/dist/converters/convertMessage.d.ts.map +1 -1
  4. package/dist/converters/convertMessage.js +15 -4
  5. package/dist/converters/convertMessage.js.map +1 -1
  6. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  7. package/dist/converters/toCreateMessage.js +2 -1
  8. package/dist/converters/toCreateMessage.js.map +1 -1
  9. package/dist/runtime/AISDKChat.d.ts.map +1 -1
  10. package/dist/runtime/AISDKChat.js.map +1 -1
  11. package/dist/runtime/AISDKThreads.js.map +1 -1
  12. package/dist/runtime/sdkIdentity.js +1 -1
  13. package/dist/runtime/useAISDKRuntime.d.ts +8 -1
  14. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  15. package/dist/runtime/useAISDKRuntime.js +212 -34
  16. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  17. package/dist/runtime/useChatRuntime.js +1 -1
  18. package/dist/runtime/useChatRuntime.js.map +1 -1
  19. package/dist/runtime/useChatThread.d.ts.map +1 -1
  20. package/dist/runtime/useChatThread.js +40 -10
  21. package/dist/runtime/useChatThread.js.map +1 -1
  22. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  23. package/dist/runtime/useExternalHistory.js +2 -1
  24. package/dist/runtime/useExternalHistory.js.map +1 -1
  25. package/dist/runtime/useResourceCleanup.js.map +1 -1
  26. package/dist/runtime/useStreamingTiming.js.map +1 -1
  27. package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
  28. package/dist/transport/AssistantChatTransport.js +9 -2
  29. package/dist/transport/AssistantChatTransport.js.map +1 -1
  30. package/dist/transport/resumable.js.map +1 -1
  31. package/dist/usage.js.map +1 -1
  32. package/package.json +15 -12
  33. package/src/converters/convertMessage.test.ts +99 -0
  34. package/src/converters/convertMessage.ts +27 -2
  35. package/src/converters/toCreateMessage.test.ts +13 -0
  36. package/src/converters/toCreateMessage.ts +1 -0
  37. package/src/runtime/AISDKChat.ts +0 -4
  38. package/src/runtime/AISDKThreads.test.ts +26 -0
  39. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1588 -19
  40. package/src/runtime/useAISDKRuntime.approval.test.tsx +27 -0
  41. package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
  42. package/src/runtime/useAISDKRuntime.ts +397 -28
  43. package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
  44. package/src/runtime/useChatRuntime.integration.test.tsx +134 -112
  45. package/src/runtime/useChatRuntime.test.ts +7 -7
  46. package/src/runtime/useChatThread.test.ts +166 -2
  47. package/src/runtime/useChatThread.transport.test.tsx +5 -2
  48. package/src/runtime/useChatThread.ts +56 -17
  49. package/src/runtime/useExternalHistory.ts +12 -1
  50. package/src/transport/AssistantChatTransport.test.ts +164 -0
  51. package/src/transport/AssistantChatTransport.ts +22 -2
@@ -970,6 +970,69 @@ describe("AISDKMessageConverter", () => {
970
970
  expect(toolCall?.argsText).toBe('{"city":"NYC');
971
971
  });
972
972
 
973
+ it("strips exactly the trailing run of closing characters", () => {
974
+ const convertArgsText = (text: string) => {
975
+ stableStringifySpy.mockReturnValueOnce(text);
976
+ const converted = AISDKMessageConverter.toThreadMessages([
977
+ {
978
+ id: "a1",
979
+ role: "assistant",
980
+ parts: [
981
+ {
982
+ type: "tool-weather",
983
+ toolCallId: "tc-1",
984
+ state: "input-streaming",
985
+ input: {},
986
+ },
987
+ ],
988
+ },
989
+ ] as any);
990
+ return (converted[0]?.content[0] as any)?.argsText;
991
+ };
992
+ const alphabet = ["}", "]", '"', "a", "\\", "{", "[", "\n", " ", "😀"];
993
+ let seed = 7;
994
+ const next = () => (seed = (seed * 48271) % 0x7fffffff);
995
+ const texts = ["", "}", '"]}', '{"a":"x"}', '{"a":"}}x"}', "\uD800}"];
996
+ for (let i = 0; i < 300; i++) {
997
+ texts.push(
998
+ Array.from(
999
+ { length: next() % 12 },
1000
+ () => alphabet[next() % alphabet.length],
1001
+ ).join(""),
1002
+ );
1003
+ }
1004
+
1005
+ for (const text of texts) {
1006
+ expect(convertArgsText(text)).toBe(text.replace(/[}\]"]+$/, ""));
1007
+ }
1008
+ });
1009
+
1010
+ it(
1011
+ "strips a long run of closing characters inside streaming input",
1012
+ { timeout: 5_000 },
1013
+ () => {
1014
+ const run = "}".repeat(200_000);
1015
+ const converted = AISDKMessageConverter.toThreadMessages([
1016
+ {
1017
+ id: "a1",
1018
+ role: "assistant",
1019
+ parts: [
1020
+ {
1021
+ type: "tool-weather",
1022
+ toolCallId: "tc-1",
1023
+ state: "input-streaming",
1024
+ input: { code: `${run}x` },
1025
+ },
1026
+ ],
1027
+ },
1028
+ ] as any);
1029
+
1030
+ expect((converted[0]?.content[0] as any)?.argsText).toBe(
1031
+ `{"code":"${run}x`,
1032
+ );
1033
+ },
1034
+ );
1035
+
973
1036
  it("attaches partial-JSON meta marking the trailing streaming field", () => {
974
1037
  const converted = AISDKMessageConverter.toThreadMessages([
975
1038
  {
@@ -1420,6 +1483,42 @@ describe("AISDKMessageConverter", () => {
1420
1483
  expect(converted[0]?.content[2]).not.toHaveProperty("providerMetadata");
1421
1484
  });
1422
1485
 
1486
+ it("keeps a step scoped reasoning block id off the part", () => {
1487
+ const converted = AISDKMessageConverter.toThreadMessages([
1488
+ {
1489
+ id: "a1",
1490
+ role: "assistant",
1491
+ parts: [
1492
+ { type: "reasoning", id: "reasoning-0", text: "first step" },
1493
+ { type: "text", text: "answer" },
1494
+ { type: "reasoning", id: "reasoning-0", text: "second step" },
1495
+ ],
1496
+ } as any,
1497
+ ]);
1498
+
1499
+ expect(converted[0]?.content[0]).toMatchObject({ type: "reasoning" });
1500
+ expect(converted[0]?.content[0]).not.toHaveProperty("id");
1501
+ expect(converted[0]?.content[1]).not.toHaveProperty("id");
1502
+ expect(converted[0]?.content[2]).not.toHaveProperty("id");
1503
+ });
1504
+
1505
+ it("forwards data part ids", () => {
1506
+ const converted = AISDKMessageConverter.toThreadMessages([
1507
+ {
1508
+ id: "a1",
1509
+ role: "assistant",
1510
+ parts: [{ type: "data-chart", id: "data-1", data: { x: 1 } }],
1511
+ } as any,
1512
+ ]);
1513
+
1514
+ expect(converted[0]?.content[0]).toMatchObject({
1515
+ type: "data",
1516
+ id: "data-1",
1517
+ name: "chart",
1518
+ data: { x: 1 },
1519
+ });
1520
+ });
1521
+
1423
1522
  it("maps TextUIPart.state onto the per-part status", () => {
1424
1523
  const converted = AISDKMessageConverter.toThreadMessages([
1425
1524
  {
@@ -76,12 +76,16 @@ export type AISDKMessageConverterMetadata =
76
76
  toolInteractions?: ReadonlyMap<string, Unstable_ToolInteractionLog>;
77
77
  supportsRichToolApprovalResponses?: boolean;
78
78
  toolApprovalResponses?: ReadonlyMap<string, RespondToToolApprovalOptions>;
79
+ cancelledToolApprovalIds?: ReadonlySet<string>;
80
+ cancelledStatusMessageIds?: ReadonlySet<string>;
79
81
  /** Id of the currently-streaming message, flagged optimistic (#4037). */
80
82
  optimisticMessageId?: string | undefined;
81
83
  };
82
84
 
83
85
  function stripClosingDelimiters(json: string): string {
84
- return json.replace(/[}\]"]+$/, "");
86
+ let end = json.length;
87
+ while (end > 0 && '}]"'.includes(json[end - 1]!)) end--;
88
+ return json.slice(0, end);
85
89
  }
86
90
 
87
91
  const MCP_APP_METADATA_CACHE_MAX = 100;
@@ -258,15 +262,23 @@ function getToolApprovalAndInterrupt(
258
262
  toolApprovalResponses:
259
263
  | ReadonlyMap<string, RespondToToolApprovalOptions>
260
264
  | undefined,
265
+ cancelledToolApprovalIds: ReadonlySet<string> | undefined,
261
266
  ): {
262
267
  approval?: NonNullable<ToolCallMessagePart["approval"]>;
263
268
  interrupt?: NonNullable<ToolCallMessagePart["interrupt"]>;
264
269
  } {
265
270
  if (part.approval) {
266
- const approval = {
271
+ const rawApproval = {
267
272
  ...readApprovalDescriptor(part.approval.descriptor),
268
273
  ...part.approval,
269
274
  };
275
+ const approval =
276
+ typeof rawApproval.id === "string" &&
277
+ rawApproval.approved === undefined &&
278
+ rawApproval.resolution === undefined &&
279
+ cancelledToolApprovalIds?.has(rawApproval.id)
280
+ ? { ...rawApproval, resolution: "cancelled" as const }
281
+ : rawApproval;
270
282
  const response =
271
283
  typeof approval.id === "string" &&
272
284
  approval.approved === undefined &&
@@ -523,6 +535,7 @@ function convertParts(
523
535
  toolStatus,
524
536
  metadata.supportsRichToolApprovalResponses === true,
525
537
  metadata.toolApprovalResponses,
538
+ metadata.cancelledToolApprovalIds,
526
539
  ),
527
540
  } satisfies ToolCallMessagePart;
528
541
  }
@@ -572,6 +585,9 @@ function convertParts(
572
585
  if (part.type.startsWith("data-")) {
573
586
  return {
574
587
  type: "data",
588
+ ...("id" in part && typeof part.id === "string"
589
+ ? { id: part.id }
590
+ : undefined),
575
591
  name: part.type.substring(5),
576
592
  data: (part as any).data,
577
593
  } satisfies DataMessagePart;
@@ -661,6 +677,15 @@ export const AISDKMessageConverter = unstable_createMessageConverter(
661
677
  id: message.id,
662
678
  createdAt,
663
679
  content,
680
+ ...(message.role === "assistant" &&
681
+ metadata.cancelledStatusMessageIds?.has(message.id)
682
+ ? {
683
+ status: {
684
+ type: "incomplete" as const,
685
+ reason: "cancelled" as const,
686
+ },
687
+ }
688
+ : undefined),
664
689
  metadata: {
665
690
  ...toThreadMetadata(message.metadata),
666
691
  ...(timing && { timing }),
@@ -185,6 +185,19 @@ describe("toCreateMessage", () => {
185
185
  ]);
186
186
  });
187
187
 
188
+ it("forwards a data part id", () => {
189
+ const message = {
190
+ ...baseMessage,
191
+ content: [
192
+ { type: "data", name: "workflow", id: "p1", data: { field: 1 } },
193
+ ],
194
+ } as unknown as AppendMessage;
195
+
196
+ expect(toCreateMessage(message).parts).toEqual([
197
+ { type: "data-workflow", id: "p1", data: { field: 1 } },
198
+ ]);
199
+ });
200
+
188
201
  it("converts a data part inside an attachment without throwing", () => {
189
202
  const message = {
190
203
  ...baseMessage,
@@ -86,6 +86,7 @@ export const toCreateMessage = <UI_MESSAGE extends UIMessage = UIMessage>(
86
86
  return {
87
87
  type: `data-${part.name}`,
88
88
  data: part.data,
89
+ ...(part.id !== undefined && { id: part.id }),
89
90
  };
90
91
  default:
91
92
  throw new Error(`Unsupported part type: ${part.type}`);
@@ -18,10 +18,6 @@ const useAISDKChat = <UI_MESSAGE extends UIMessage = UIMessage>(
18
18
  options?: AISDKChatOptions<UI_MESSAGE>,
19
19
  ) => {
20
20
  const [id] = useState(() => options?.id ?? generateId());
21
- // The transport resolves the request id from the thread list item, falling
22
- // back to the runtime's main item, whose id here is the external store's
23
- // placeholder constant. The single thread of this entry is the chat itself,
24
- // so the handed-over item initializes to the chat id.
25
21
  const [threadListItem] = useState(() => ({
26
22
  initialize: async () => ({ remoteId: id, externalId: undefined }),
27
23
  }));
@@ -56,6 +56,32 @@ const chatCallbacks = () => ({
56
56
  });
57
57
 
58
58
  describe("AISDKThreads", () => {
59
+ it("emits the selected thread id when its composer sends", async () => {
60
+ const { transport } = createControlledTransport();
61
+ const handle = createAssistantClient(
62
+ AuiConfig({ threads: AISDKThreads({ transport }) }),
63
+ );
64
+ try {
65
+ handle.subscribe(() => {});
66
+ const aui = handle.getClient();
67
+ const sent = vi.fn();
68
+ aui.on({ scope: "thread", event: "composer.send" }, sent);
69
+
70
+ flushTapSync(() => aui.composer.setText("hello"));
71
+ flushTapSync(() => aui.composer.send());
72
+
73
+ await vi.waitFor(() => {
74
+ expect(sent).toHaveBeenCalledWith(
75
+ expect.objectContaining({
76
+ threadId: aui.threads.getState().mainThreadId,
77
+ }),
78
+ );
79
+ });
80
+ } finally {
81
+ handle.destroy();
82
+ }
83
+ });
84
+
59
85
  it("runs one chat per thread and keeps histories isolated across switches", async () => {
60
86
  const { transport, emit, close } = createControlledTransport();
61
87
  const handle = createAssistantClient(