@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
@@ -3,6 +3,7 @@ import {
3
3
  isReasoningFileUIPart,
4
4
  isCustomContentUIPart,
5
5
  getToolName,
6
+ type FileUIPart,
6
7
  type UIMessage,
7
8
  } from "ai";
8
9
  import {
@@ -13,6 +14,8 @@ import {
13
14
  isMcpAppUri,
14
15
  type ReasoningMessagePart,
15
16
  type ToolApprovalOption,
17
+ type ToolApprovalQuestion,
18
+ type ToolApprovalQuestionOption,
16
19
  type ToolCallMessagePart,
17
20
  type TextMessagePart,
18
21
  type DataMessagePart,
@@ -26,12 +29,14 @@ import {
26
29
  type RespondToToolApprovalOptions,
27
30
  type Unstable_ToolInteractionLog,
28
31
  } from "@assistant-ui/core";
32
+ import { normalizeToolApprovalAnswers } from "./toolApprovalAnswers";
29
33
  import { stableStringifyToolArgs } from "@assistant-ui/core/internal";
34
+ import { markPartialJsonObjectComplete } from "assistant-stream/internal";
30
35
  import {
31
36
  parsePartialJsonObject,
32
37
  type ReadonlyJSONObject,
33
38
  } from "assistant-stream/utils";
34
- import { unwrapModelContentEnvelope } from "./modelContentEnvelope";
39
+ import { unwrapModelContentEnvelope } from "assistant-stream/internal";
35
40
 
36
41
  type MessageMetadata = ThreadMessageLike["metadata"];
37
42
 
@@ -63,13 +68,15 @@ export type AISDKMessageConverterMetadata =
63
68
  useExternalMessageConverter.Metadata & {
64
69
  toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;
65
70
  /**
66
- * Frozen `argsText` keyed weakly by a settled tool call's input object, then
67
- * by call, since the text carries the call's streamed key order. A known
68
- * call/input pair skips serialization; the entries become collectible once
69
- * the input is unreachable. A fresh input object re-serializes in its own
70
- * deterministic key order.
71
+ * Frozen text and completion-marked args keyed weakly by a settled tool
72
+ * call's input object, then by call, since the text carries its streamed key
73
+ * order. A known call/input pair skips serialization and parsing; entries
74
+ * become collectible once the input is unreachable.
71
75
  */
72
- toolArgsTextCache?: WeakMap<ReadonlyJSONObject, Map<string, string>>;
76
+ toolArgsTextCache?: WeakMap<
77
+ ReadonlyJSONObject,
78
+ Map<string, { argsText: string; args: ReadonlyJSONObject }>
79
+ >;
73
80
  toolLastInputCache?: Map<string, ReadonlyJSONObject>;
74
81
  mcpAppMetadataCache?: Map<string, McpAppMetadata>;
75
82
  toolArtifacts?: ReadonlyMap<string, unknown>;
@@ -95,17 +102,45 @@ function extractMcpAppMetadata(
95
102
  cache: Map<string, McpAppMetadata> | undefined,
96
103
  ): McpAppMetadata | undefined {
97
104
  if (!part || typeof part !== "object") return undefined;
105
+ const toolMetadata = (part as { toolMetadata?: unknown }).toolMetadata;
106
+ const toolApp =
107
+ toolMetadata && typeof toolMetadata === "object"
108
+ ? (toolMetadata as { app?: unknown }).app
109
+ : undefined;
98
110
  const meta = (part as { callProviderMetadata?: unknown })
99
111
  .callProviderMetadata;
100
112
  const mcp =
101
113
  meta && typeof meta === "object"
102
114
  ? (meta as { mcp?: unknown }).mcp
103
115
  : undefined;
104
- const app =
116
+ const providerApp =
105
117
  mcp && typeof mcp === "object" ? (mcp as { app?: unknown }).app : undefined;
118
+
119
+ const apps: Record<string, unknown>[] = [];
120
+ for (const candidate of [toolApp, providerApp]) {
121
+ if (!candidate || typeof candidate !== "object") continue;
122
+ const candidateApp = candidate as Record<string, unknown>;
123
+ const resourceUri = candidateApp["resourceUri"];
124
+ if (typeof resourceUri !== "string" || !isMcpAppUri(resourceUri)) continue;
125
+ apps.push({
126
+ resourceUri,
127
+ ...(typeof candidateApp["mimeType"] === "string" && {
128
+ mimeType: candidateApp["mimeType"],
129
+ }),
130
+ ...(Array.isArray(candidateApp["visibility"]) && {
131
+ visibility: candidateApp["visibility"],
132
+ }),
133
+ ...(candidate === providerApp &&
134
+ typeof candidateApp["serverId"] === "string" &&
135
+ candidateApp["serverId"].length > 0 && {
136
+ serverId: candidateApp["serverId"],
137
+ }),
138
+ });
139
+ }
140
+
106
141
  let a: Record<string, unknown>;
107
- if (app && typeof app === "object") {
108
- a = app as Record<string, unknown>;
142
+ if (apps.length > 0) {
143
+ a = { ...(apps[1] ?? {}), ...apps[0]! };
109
144
  } else {
110
145
  // MCP-UI tools surface the pointer on result._meta: canonical nested
111
146
  // `ui.resourceUri`, or the deprecated flat `"ui/resourceUri"` key.
@@ -221,6 +256,51 @@ const normalizeToolApprovalOptions = (
221
256
  });
222
257
  };
223
258
 
259
+ const normalizeToolApprovalQuestions = (
260
+ questions: unknown,
261
+ ): readonly ToolApprovalQuestion[] | undefined => {
262
+ if (!Array.isArray(questions)) return undefined;
263
+
264
+ return questions.flatMap<ToolApprovalQuestion>((value) => {
265
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
266
+ const question = value as Record<string, unknown>;
267
+ if (typeof question.id !== "string" || typeof question.prompt !== "string")
268
+ return [];
269
+ const options = Array.isArray(question.options)
270
+ ? question.options.flatMap<ToolApprovalQuestionOption>((option) => {
271
+ if (!option || typeof option !== "object" || Array.isArray(option))
272
+ return [];
273
+ const { id, label, description } = option as Record<string, unknown>;
274
+ if (typeof id !== "string" || typeof label !== "string") return [];
275
+ return [
276
+ {
277
+ id,
278
+ label,
279
+ ...(typeof description === "string" && { description }),
280
+ },
281
+ ];
282
+ })
283
+ : undefined;
284
+
285
+ return [
286
+ {
287
+ id: question.id,
288
+ prompt: question.prompt,
289
+ ...(typeof question.header === "string" && {
290
+ header: question.header,
291
+ }),
292
+ ...(options && { options }),
293
+ ...(typeof question.multiple === "boolean" && {
294
+ multiple: question.multiple,
295
+ }),
296
+ ...(typeof question.allowFreeform === "boolean" && {
297
+ allowFreeform: question.allowFreeform,
298
+ }),
299
+ },
300
+ ];
301
+ });
302
+ };
303
+
224
304
  const APPROVAL_DESCRIPTOR_FIELDS = [
225
305
  "prompt",
226
306
  "display",
@@ -229,6 +309,8 @@ const APPROVAL_DESCRIPTOR_FIELDS = [
229
309
  "options",
230
310
  "optionId",
231
311
  "text",
312
+ "questions",
313
+ "answers",
232
314
  "resolution",
233
315
  ] as const;
234
316
 
@@ -302,6 +384,8 @@ function getToolApprovalAndInterrupt(
302
384
  options,
303
385
  optionId,
304
386
  text,
387
+ questions,
388
+ answers,
305
389
  ...additionalApprovalFields
306
390
  } = response
307
391
  ? {
@@ -310,11 +394,18 @@ function getToolApprovalAndInterrupt(
310
394
  ...(response.reason != null && { reason: response.reason }),
311
395
  ...(response.optionId != null && { optionId: response.optionId }),
312
396
  ...(response.text != null && { text: response.text }),
397
+ ...(response.answers != null && { answers: response.answers }),
313
398
  }
314
399
  : approval;
315
400
  const normalizedOptions = supportsRichToolApprovalResponses
316
401
  ? normalizeToolApprovalOptions(options)
317
402
  : undefined;
403
+ const normalizedQuestions = supportsRichToolApprovalResponses
404
+ ? normalizeToolApprovalQuestions(questions)
405
+ : undefined;
406
+ const normalizedAnswers = supportsRichToolApprovalResponses
407
+ ? normalizeToolApprovalAnswers(answers)
408
+ : undefined;
318
409
  const requestReason = additionalApprovalFields.requestReason;
319
410
  if (typeof id === "string")
320
411
  return {
@@ -332,12 +423,15 @@ function getToolApprovalAndInterrupt(
332
423
  ...(supportsRichToolApprovalResponses && {
333
424
  ...((display === "decision" ||
334
425
  display === "select" ||
335
- display === "text") && { display }),
426
+ display === "text" ||
427
+ display === "questions") && { display }),
336
428
  ...(typeof allowFreeform === "boolean" && { allowFreeform }),
337
429
  ...(typeof dismissible === "boolean" && { dismissible }),
338
430
  ...(normalizedOptions && { options: normalizedOptions }),
339
431
  ...(typeof optionId === "string" && { optionId }),
340
432
  ...(typeof text === "string" && { text }),
433
+ ...(normalizedQuestions && { questions: normalizedQuestions }),
434
+ ...(normalizedAnswers && { answers: normalizedAnswers }),
341
435
  }),
342
436
  ...((resolution === "cancelled" || resolution === "expired") && {
343
437
  resolution,
@@ -367,6 +461,25 @@ const uiPartStateToStatus = (
367
461
  return undefined;
368
462
  };
369
463
 
464
+ const toSystemContent = (content: MessageContent): MessageContent => {
465
+ const text = content.filter((part) => part.type === "text");
466
+ if (text.length === 1) return text;
467
+ const providerMetadata = text.reduce<PartProviderMetadata>(
468
+ (merged, part) =>
469
+ part.providerMetadata != null
470
+ ? { ...merged, ...part.providerMetadata }
471
+ : merged,
472
+ {},
473
+ );
474
+ return [
475
+ {
476
+ type: "text",
477
+ text: text.map((part) => part.text).join(""),
478
+ ...(Object.keys(providerMetadata).length > 0 && { providerMetadata }),
479
+ },
480
+ ];
481
+ };
482
+
370
483
  function convertParts(
371
484
  message: UIMessage,
372
485
  metadata: AISDKMessageConverterMetadata,
@@ -378,6 +491,7 @@ function convertParts(
378
491
  const converted = message.parts
379
492
  .filter(
380
493
  (p) =>
494
+ typeof p?.type === "string" &&
381
495
  p.type !== "step-start" &&
382
496
  (message.role !== "user" || p.type !== "file"),
383
497
  )
@@ -386,7 +500,7 @@ function convertParts(
386
500
  const status = uiPartStateToStatus(part.state);
387
501
  return {
388
502
  type: "text",
389
- text: part.text,
503
+ text: part.text ?? "",
390
504
  ...(status != null ? { status } : undefined),
391
505
  ...(part.providerMetadata != null
392
506
  ? {
@@ -411,7 +525,7 @@ function convertParts(
411
525
  }
412
526
 
413
527
  if (isToolUIPart(part)) {
414
- const toolName = getToolName(part);
528
+ const toolName = getToolName(part) ?? "";
415
529
  const toolCallId = part.toolCallId;
416
530
  const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;
417
531
 
@@ -475,20 +589,31 @@ function convertParts(
475
589
  // re-serializing large args while the call keeps that input. Arrival
476
590
  // order only matters while args stream, so the key-order entry is
477
591
  // released.
592
+ const inputArgs = args;
478
593
  const frozen =
479
- metadata.toolArgsTextCache?.get(args) ?? new Map<string, string>();
480
- const frozenText = frozen.get(argsKeyOrderCacheKey);
481
- if (frozenText !== undefined) {
482
- argsText = frozenText;
594
+ metadata.toolArgsTextCache?.get(inputArgs) ??
595
+ new Map<string, { argsText: string; args: ReadonlyJSONObject }>();
596
+ const frozenEntry = frozen.get(argsKeyOrderCacheKey);
597
+ if (frozenEntry !== undefined) {
598
+ argsText = frozenEntry.argsText;
599
+ args = frozenEntry.args;
483
600
  } else {
484
601
  argsText = stableStringifyToolArgs(
485
602
  metadata.toolArgsKeyOrderCache,
486
603
  argsKeyOrderCacheKey,
487
604
  args,
488
605
  );
606
+ // The input is final even while execution keeps the part running.
607
+ // Other runtimes can synthesize complete JSON text from an
608
+ // accumulating snapshot, so only this converter supplies the
609
+ // completion signal it knows from the AI SDK part state.
610
+ // A complete root marker settles every field without parsing the
611
+ // serialized text again. Keep the SDK input's nested identities
612
+ // and own fields, including prototype-named JSON keys.
613
+ args = markPartialJsonObjectComplete(args);
489
614
  metadata.toolArgsTextCache?.set(
490
- args,
491
- frozen.set(argsKeyOrderCacheKey, argsText),
615
+ inputArgs,
616
+ frozen.set(argsKeyOrderCacheKey, { argsText, args }),
492
617
  );
493
618
  }
494
619
  metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);
@@ -557,10 +682,11 @@ function convertParts(
557
682
  }
558
683
 
559
684
  if (part.type === "file") {
685
+ if (typeof part.url !== "string") return null;
560
686
  return {
561
687
  type: "file",
562
688
  data: part.url,
563
- mimeType: part.mediaType,
689
+ mimeType: part.mediaType ?? "unknown/unknown",
564
690
  ...(part.filename != null && { filename: part.filename }),
565
691
  } satisfies FileMessagePart;
566
692
  }
@@ -594,10 +720,11 @@ function convertParts(
594
720
  }
595
721
 
596
722
  if (isReasoningFileUIPart(part)) {
723
+ if (typeof part.url !== "string") return null;
597
724
  return {
598
725
  type: "file",
599
726
  data: part.url,
600
- mimeType: part.mediaType,
727
+ mimeType: part.mediaType ?? "unknown/unknown",
601
728
  } satisfies FileMessagePart;
602
729
  }
603
730
 
@@ -637,7 +764,10 @@ export const AISDKMessageConverter = unstable_createMessageConverter(
637
764
  createdAt,
638
765
  content,
639
766
  attachments: message.parts
640
- ?.filter((p) => p.type === "file")
767
+ ?.filter(
768
+ (p): p is FileUIPart =>
769
+ p?.type === "file" && typeof p.url === "string",
770
+ )
641
771
  .map((part, idx) => {
642
772
  const mediaType = part.mediaType ?? "unknown/unknown";
643
773
  const isImage = mediaType.startsWith("image/");
@@ -676,7 +806,8 @@ export const AISDKMessageConverter = unstable_createMessageConverter(
676
806
  role: message.role,
677
807
  id: message.id,
678
808
  createdAt,
679
- content,
809
+ content:
810
+ message.role === "system" ? toSystemContent(content) : content,
680
811
  ...(message.role === "assistant" &&
681
812
  metadata.cancelledStatusMessageIds?.has(message.id)
682
813
  ? {
@@ -11,6 +11,33 @@ const baseMessage = {
11
11
  } as const;
12
12
 
13
13
  describe("toCreateMessage", () => {
14
+ it.each([
15
+ [
16
+ "data:;base64,SGVsbG8=",
17
+ "data:application/octet-stream;base64,SGVsbG8=",
18
+ "application/octet-stream",
19
+ ],
20
+ [
21
+ "data:;charset=utf-8,hello",
22
+ "data:text/plain;charset=utf-8,hello",
23
+ "text/plain",
24
+ ],
25
+ [
26
+ "data:;base64,",
27
+ "data:application/octet-stream;base64,",
28
+ "application/octet-stream",
29
+ ],
30
+ ])("uses the resolved media type for %s", (data, url, mediaType) => {
31
+ const message = {
32
+ ...baseMessage,
33
+ content: [{ type: "file", data, mimeType: "" }],
34
+ } as unknown as AppendMessage;
35
+
36
+ expect(toCreateMessage(message).parts).toEqual([
37
+ { type: "file", url, mediaType },
38
+ ]);
39
+ });
40
+
14
41
  it("converts a direct file part in message content", () => {
15
42
  const message = {
16
43
  ...baseMessage,
@@ -0,0 +1,27 @@
1
+ import type { ToolApprovalAnswer } from "@assistant-ui/core";
2
+
3
+ export const normalizeToolApprovalAnswers = (
4
+ answers: unknown,
5
+ ): Readonly<Record<string, ToolApprovalAnswer>> | undefined => {
6
+ if (!answers || typeof answers !== "object" || Array.isArray(answers))
7
+ return undefined;
8
+
9
+ return Object.fromEntries(
10
+ Object.entries(answers).flatMap(([id, value]) => {
11
+ if (!value || typeof value !== "object" || Array.isArray(value))
12
+ return [];
13
+ const { optionIds, text } = value as Record<string, unknown>;
14
+ const answer: ToolApprovalAnswer = {
15
+ ...(Array.isArray(optionIds) && {
16
+ optionIds: optionIds.filter(
17
+ (optionId): optionId is string => typeof optionId === "string",
18
+ ),
19
+ }),
20
+ ...(typeof text === "string" && { text }),
21
+ };
22
+ return answer.optionIds?.length || answer.text !== undefined
23
+ ? [[id, answer]]
24
+ : [];
25
+ }),
26
+ );
27
+ };
@@ -12,7 +12,7 @@ export type {
12
12
  AssistantChatResumableOptions,
13
13
  ResumableClientStorage,
14
14
  } from "./transport/resumable";
15
- export { frontendTools, type FrontendTools } from "./tools/frontendTools";
15
+ export { frontendTools, type FrontendTools } from "assistant-stream/ai-sdk";
16
16
  export { injectQuoteContext } from "./model-context/injectQuoteContext";
17
17
  export { unstable_injectInteractableContext } from "./model-context/injectInteractableContext";
18
18
  export type { ThreadTokenUsage, TokenUsageExtractableMessage } from "./usage";
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@ export type {
12
12
  AssistantChatResumableOptions,
13
13
  ResumableClientStorage,
14
14
  } from "./transport/resumable";
15
- export { frontendTools, type FrontendTools } from "./tools/frontendTools";
15
+ export { frontendTools, type FrontendTools } from "assistant-stream/ai-sdk";
16
16
  export { injectQuoteContext } from "./model-context/injectQuoteContext";
17
17
  export { unstable_injectInteractableContext } from "./model-context/injectInteractableContext";
18
18
  export type { ThreadTokenUsage, TokenUsageExtractableMessage } from "./usage";
@@ -20,6 +20,8 @@ import {
20
20
  * `format` must also handle entries with `partial: true`, whose `state` carries
21
21
  * only the fields that changed since the model's last known state.
22
22
  *
23
+ * @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
24
+ *
23
25
  * @example
24
26
  * ```ts
25
27
  * import { convertToModelMessages, streamText } from "ai";
@@ -28,6 +30,7 @@ import {
28
30
  * export async function POST(req: Request) {
29
31
  * const { messages } = await req.json();
30
32
  * const result = streamText({
33
+ * abortSignal: req.signal,
31
34
  * model: myModel,
32
35
  * messages: await convertToModelMessages(unstable_injectInteractableContext(messages)),
33
36
  * });
@@ -27,6 +27,7 @@ const getQuoteText = (metadata: unknown): string | undefined => {
27
27
  * export async function POST(req: Request) {
28
28
  * const { messages } = await req.json();
29
29
  * const result = streamText({
30
+ * abortSignal: req.signal,
30
31
  * model: myModel,
31
32
  * messages: await convertToModelMessages(injectQuoteContext(messages)),
32
33
  * });
@@ -1,10 +1,11 @@
1
+ import { flushTapSync } from "@assistant-ui/tap";
1
2
  // @vitest-environment jsdom
2
3
 
3
- import { StrictMode, type ReactNode } from "react";
4
+ import { StrictMode, useLayoutEffect, type ReactNode } from "react";
4
5
  import { act, render, waitFor } from "@testing-library/react";
5
6
  import { AuiConfig, AuiProvider, useAui } from "@assistant-ui/store";
6
7
  import type { ChatTransport, UIMessage } from "ai";
7
- import { describe, expect, it } from "vitest";
8
+ import { describe, expect, it, vi } from "vitest";
8
9
  import { AISDKChat } from "./AISDKChat";
9
10
  import {
10
11
  createCancellableTransport,
@@ -66,3 +67,57 @@ describe("AISDKChat legacy useAui host integration", () => {
66
67
  await waitFor(() => expect(getCancelCount()).toBe(1));
67
68
  });
68
69
  });
70
+
71
+ describe("replacement transports", () => {
72
+ it("routes sends through a replacement transport", async () => {
73
+ const emptyStream = () =>
74
+ new ReadableStream({ start: (controller) => controller.close() });
75
+ const sendA = vi.fn(async () => emptyStream());
76
+ const sendB = vi.fn(async () => emptyStream());
77
+ const transportA: ChatTransport<UIMessage> = {
78
+ sendMessages: sendA,
79
+ reconnectToStream: vi.fn(),
80
+ };
81
+ const transportB: ChatTransport<UIMessage> = {
82
+ sendMessages: sendB,
83
+ reconnectToStream: vi.fn(),
84
+ };
85
+ let initialClient: ReturnType<typeof useAui> | undefined;
86
+ let currentClient: ReturnType<typeof useAui> | undefined;
87
+ const CaptureClient = () => {
88
+ const aui = useAui();
89
+ initialClient ??= aui;
90
+ currentClient = aui;
91
+ return null;
92
+ };
93
+ const SendOnLayout = () => {
94
+ const aui = useAui();
95
+ useLayoutEffect(() => {
96
+ flushTapSync(() => {
97
+ aui.composer.setText("hello");
98
+ aui.composer.send();
99
+ });
100
+ }, [aui]);
101
+ return null;
102
+ };
103
+ const App = ({
104
+ transport,
105
+ send = false,
106
+ }: {
107
+ transport: ChatTransport<UIMessage>;
108
+ send?: boolean;
109
+ }) => (
110
+ <AuiProvider config={AuiConfig({ threads: AISDKChat({ transport }) })}>
111
+ <CaptureClient />
112
+ {send && <SendOnLayout />}
113
+ </AuiProvider>
114
+ );
115
+
116
+ const view = render(<App transport={transportA} />);
117
+ view.rerender(<App transport={transportB} send />);
118
+
119
+ await waitFor(() => expect(sendB).toHaveBeenCalledOnce());
120
+ expect(sendA).not.toHaveBeenCalled();
121
+ expect(currentClient).toBe(initialClient);
122
+ });
123
+ });
@@ -79,10 +79,12 @@ import { AI_SDK_SDK } from "./sdkIdentity";
79
79
  describe("AISDKThreads cloud", () => {
80
80
  it("reloads history when switching a keyed cloud thread", async () => {
81
81
  const cloud = {} as AssistantCloud;
82
+ const scopeId = "workspace-1";
82
83
  const handle = createAssistantClient(
83
84
  AuiConfig({
84
85
  threads: AISDKThreads({
85
86
  cloud,
87
+ scopeId,
86
88
  threadId: "t1",
87
89
  }),
88
90
  }),
@@ -98,6 +100,7 @@ describe("AISDKThreads cloud", () => {
98
100
  });
99
101
  expect(mocks.useCloudThreadListAdapter).toHaveBeenCalledWith({
100
102
  cloud,
103
+ scopeId,
101
104
  sdk: AI_SDK_SDK,
102
105
  });
103
106
  const afterFirst = load.mock.calls.length;