@assistant-ui/ai-sdk 0.0.6 → 0.0.8

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 (107) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
  3. package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
  4. package/dist/adapters/vercelAttachmentAdapter.d.ts +2 -5
  5. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
  6. package/dist/adapters/vercelAttachmentAdapter.js +3 -3
  7. package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
  8. package/dist/aiSDKExtras.d.ts +6 -9
  9. package/dist/aiSDKExtras.d.ts.map +1 -1
  10. package/dist/converters/convertMessage.d.ts +34 -25
  11. package/dist/converters/convertMessage.d.ts.map +1 -1
  12. package/dist/converters/convertMessage.js +78 -8
  13. package/dist/converters/convertMessage.js.map +1 -1
  14. package/dist/converters/modelContentEnvelope.d.ts +10 -12
  15. package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
  16. package/dist/converters/toCreateMessage.d.ts +3 -6
  17. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  18. package/dist/converters/toolOutputConversion.d.ts +22 -25
  19. package/dist/converters/toolOutputConversion.d.ts.map +1 -1
  20. package/dist/hooks.d.ts +4 -7
  21. package/dist/hooks.d.ts.map +1 -1
  22. package/dist/index.d.ts +17 -14
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.native.d.ts +15 -12
  25. package/dist/index.native.d.ts.map +1 -0
  26. package/dist/model-context/injectInteractableContext.d.ts +3 -6
  27. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  28. package/dist/model-context/injectQuoteContext.d.ts +2 -5
  29. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  30. package/dist/runtime/AISDKChat.d.ts +4 -7
  31. package/dist/runtime/AISDKChat.d.ts.map +1 -1
  32. package/dist/runtime/AISDKChat.js +1 -1
  33. package/dist/runtime/AISDKThreads.d.ts +29 -32
  34. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  35. package/dist/runtime/AISDKThreads.js +26 -33
  36. package/dist/runtime/AISDKThreads.js.map +1 -1
  37. package/dist/runtime/sdkIdentity.d.ts +2 -5
  38. package/dist/runtime/sdkIdentity.d.ts.map +1 -1
  39. package/dist/runtime/sdkIdentity.js +1 -1
  40. package/dist/runtime/useAISDKRuntime.d.ts +72 -61
  41. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  42. package/dist/runtime/useAISDKRuntime.js +170 -56
  43. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  44. package/dist/runtime/useChatRuntime.d.ts +8 -11
  45. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  46. package/dist/runtime/useChatRuntime.js +14 -10
  47. package/dist/runtime/useChatRuntime.js.map +1 -1
  48. package/dist/runtime/useChatThread.d.ts +100 -81
  49. package/dist/runtime/useChatThread.d.ts.map +1 -1
  50. package/dist/runtime/useChatThread.js +33 -13
  51. package/dist/runtime/useChatThread.js.map +1 -1
  52. package/dist/runtime/useExternalHistory.d.ts +8 -9
  53. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  54. package/dist/runtime/useExternalHistory.js +236 -58
  55. package/dist/runtime/useExternalHistory.js.map +1 -1
  56. package/dist/runtime/useResourceCleanup.d.ts +1 -4
  57. package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
  58. package/dist/runtime/useResourceCleanup.js +38 -14
  59. package/dist/runtime/useResourceCleanup.js.map +1 -1
  60. package/dist/runtime/useStreamingTiming.d.ts +5 -8
  61. package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
  62. package/dist/runtime/useStreamingTiming.js +1 -1
  63. package/dist/tools/frontendTools.d.ts +24 -27
  64. package/dist/tools/frontendTools.d.ts.map +1 -1
  65. package/dist/tools/generativeTools.d.ts +29 -32
  66. package/dist/tools/generativeTools.d.ts.map +1 -1
  67. package/dist/tools/mcp-stdio.node.d.ts +2 -2
  68. package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
  69. package/dist/tools/mcp-stdio.unsupported.d.ts +2 -5
  70. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
  71. package/dist/transport/AssistantChatTransport.d.ts +17 -20
  72. package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
  73. package/dist/transport/resumable.d.ts +29 -32
  74. package/dist/transport/resumable.d.ts.map +1 -1
  75. package/dist/usage.d.ts +12 -15
  76. package/dist/usage.d.ts.map +1 -1
  77. package/dist/usage.js.map +1 -1
  78. package/dist/utils/getVercelAIMessages.d.ts +3 -6
  79. package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
  80. package/dist/utils/sliceMessagesUntil.d.ts +2 -5
  81. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  82. package/package.json +16 -14
  83. package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
  84. package/src/adapters/vercelAttachmentAdapter.ts +2 -2
  85. package/src/converters/convertMessage.test.ts +616 -3
  86. package/src/converters/convertMessage.ts +187 -12
  87. package/src/index.native.ts +1 -1
  88. package/src/index.ts +1 -1
  89. package/src/runtime/AISDKChat.test.ts +4 -5
  90. package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
  91. package/src/runtime/AISDKThreads.test.ts +88 -51
  92. package/src/runtime/AISDKThreads.ts +4 -13
  93. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +338 -0
  94. package/src/runtime/useAISDKRuntime.approval.test.tsx +476 -4
  95. package/src/runtime/useAISDKRuntime.test.ts +282 -8
  96. package/src/runtime/useAISDKRuntime.ts +274 -46
  97. package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
  98. package/src/runtime/useChatRuntime.integration.test.tsx +56 -0
  99. package/src/runtime/useChatRuntime.test.ts +77 -5
  100. package/src/runtime/useChatRuntime.ts +6 -2
  101. package/src/runtime/useChatThread.transport.test.tsx +149 -0
  102. package/src/runtime/useChatThread.ts +77 -10
  103. package/src/runtime/useExternalHistory.test.ts +989 -3
  104. package/src/runtime/useExternalHistory.ts +390 -24
  105. package/src/runtime/useResourceCleanup.ts +41 -15
  106. package/src/tools/generativeTools.test.ts +7 -1
  107. package/src/usage.ts +1 -1
@@ -14,7 +14,7 @@ import type {
14
14
  CreateUIMessage,
15
15
  UseChatHelpers,
16
16
  } from "@ai-sdk/react";
17
- import { isToolUIPart, generateId } from "ai";
17
+ import { isToolUIPart, generateId, getToolName } from "ai";
18
18
  import {
19
19
  useExternalStoreRuntime,
20
20
  useRuntimeAdapters,
@@ -37,12 +37,15 @@ import type {
37
37
  AppendMessage,
38
38
  RunConfig,
39
39
  McpAppMetadata,
40
+ RespondToToolApprovalOptions,
41
+ Unstable_ToolInteractionLog,
40
42
  } from "@assistant-ui/core";
41
43
  import {
42
44
  getExternalStoreMessages,
43
45
  pickExternalStoreSharedOptions,
44
46
  } from "@assistant-ui/core";
45
47
  import {
48
+ appendToolInteraction,
46
49
  consumeSuggestionResult,
47
50
  MessageRepository,
48
51
  } from "@assistant-ui/core/internal";
@@ -52,7 +55,10 @@ import { sliceMessagesUntil } from "../utils/sliceMessagesUntil";
52
55
  import { toCreateMessage } from "../converters/toCreateMessage";
53
56
  import { vercelAttachmentAdapter } from "../adapters/vercelAttachmentAdapter";
54
57
  import { getVercelAIMessages } from "../utils/getVercelAIMessages";
55
- import { AISDKMessageConverter } from "../converters/convertMessage";
58
+ import {
59
+ AISDKMessageConverter,
60
+ type AISDKMessageConverterMetadata,
61
+ } from "../converters/convertMessage";
56
62
  import { wrapModelContentEnvelope } from "../converters/modelContentEnvelope";
57
63
  import {
58
64
  type AISDKStorageFormat,
@@ -81,6 +87,23 @@ const toUIMessage = <UI_MESSAGE extends UIMessage>(
81
87
  role: createMessage.role ?? fallbackRole,
82
88
  }) as UI_MESSAGE;
83
89
 
90
+ const toVoiceTranscriptUIMessage = <UI_MESSAGE extends UIMessage>(
91
+ message: ThreadMessage,
92
+ ): UI_MESSAGE =>
93
+ ({
94
+ id: message.id,
95
+ role: message.role,
96
+ parts: message.content
97
+ .filter((part) => part.type === "text")
98
+ .map((part) => ({ type: "text", text: part.text })),
99
+ metadata: {
100
+ ...(message.metadata.modality && { modality: message.metadata.modality }),
101
+ ...(Object.keys(message.metadata.custom).length > 0 && {
102
+ custom: message.metadata.custom,
103
+ }),
104
+ },
105
+ }) as UI_MESSAGE;
106
+
84
107
  export type AISDKRuntimeAdapter<UI_MESSAGE extends UIMessage = UIMessage> =
85
108
  ExternalStoreSharedOptions & {
86
109
  adapters?:
@@ -115,6 +138,24 @@ export type AISDKRuntimeAdapter<UI_MESSAGE extends UIMessage = UIMessage> =
115
138
  * Provide this to bridge resume-tool-call invocations into a custom handler.
116
139
  */
117
140
  onResumeToolCall?: ExternalStoreAdapter["onResumeToolCall"];
141
+ /**
142
+ * Answers tool approval requests through a host-owned channel instead of the AI SDK's `addToolApprovalResponse`.
143
+ *
144
+ * Called for every approval request in the thread with the complete response, including option and free-form answers. Hand requests the host does not own to `respondViaAISDK`, which is what runs when this option is omitted. The answer applies to the approval when the handler starts and is removed if it throws. It is never written into the `useChat` messages, so `sendAutomaticallyWhen` cannot forward it. With a history adapter, the answer is stored with its message once the handler resolves and returns on reload, and a second response to the same request rejects; without one it lasts as long as this runtime, and a runtime mounted again over the same chat shows the request open until the resumed run records its resolution in the chat.
145
+ *
146
+ * While a handler is set, an approval's `display`, `allowFreeform`, `dismissible` and `options` reach the renderer, because the handler can receive answers the AI SDK cannot carry. A stream declares them through the `approvalDescriptor` of its `tool-approval-request` chunk, the one approval field the AI SDK keeps opaque; the converter reads the request and answer fields from that descriptor when the approval itself lacks them.
147
+ */
148
+ onRespondToToolApproval?:
149
+ | ((
150
+ response: RespondToToolApprovalOptions,
151
+ context: {
152
+ toolCallId: string;
153
+ toolName: string;
154
+ /** Sends this response through the AI SDK's `addToolApprovalResponse`, which carries only `approved` and `reason`. */
155
+ respondViaAISDK: () => Promise<void>;
156
+ },
157
+ ) => Promise<void> | void)
158
+ | undefined;
118
159
  /**
119
160
  * How consecutive assistant messages are rendered.
120
161
  *
@@ -220,6 +261,11 @@ const useGeneratedSuggestions = (
220
261
 
221
262
  const NO_CANCELLED_MESSAGE_IDS: ReadonlySet<string> = new Set();
222
263
 
264
+ const NO_TOOL_APPROVAL_RESPONSES: ReadonlyMap<
265
+ string,
266
+ RespondToToolApprovalOptions
267
+ > = new Map();
268
+
223
269
  const toChatError = (error: Error): AssistantError => {
224
270
  const code = (error as { code?: unknown }).code;
225
271
  return {
@@ -243,6 +289,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
243
289
  cancelPendingToolCallsOnSend = true,
244
290
  onResume,
245
291
  onResumeToolCall,
292
+ onRespondToToolApproval: customOnRespondToToolApproval,
246
293
  joinStrategy,
247
294
  messageRepository,
248
295
  unstable_onBranchChange,
@@ -256,14 +303,36 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
256
303
  chatId: string;
257
304
  ids: ReadonlySet<string>;
258
305
  } | null>(null);
306
+ const [toolApprovalResponses, setToolApprovalResponses] = useState<
307
+ ReadonlyMap<string, RespondToToolApprovalOptions>
308
+ >(NO_TOOL_APPROVAL_RESPONSES);
309
+ const [toolArtifactEpoch, setToolArtifactEpoch] = useState(0);
310
+ const [toolInteractionEpoch, setToolInteractionEpoch] = useState(0);
311
+ const hostApprovalIdsRef = useRef(new Set<string>());
312
+ const toolApprovalResponsesRef = useRef<
313
+ Map<string, RespondToToolApprovalOptions>
314
+ >(new Map());
259
315
  const toolArgsKeyOrderCacheRef = useRef<Map<string, Map<string, string[]>>>(
260
316
  new Map(),
261
317
  );
262
318
  const toolLastInputCacheRef = useRef<Map<string, ReadonlyJSONObject>>(
263
319
  new Map(),
264
320
  );
321
+ const toolArgsTextCacheRef = useRef<
322
+ WeakMap<ReadonlyJSONObject, Map<string, string>>
323
+ >(new WeakMap());
265
324
  const mcpAppMetadataCacheRef = useRef<Map<string, McpAppMetadata>>(new Map());
325
+ const toolArtifactsRef = useRef<Map<string, unknown>>(new Map());
326
+ const toolInteractionsRef = useRef<Map<string, Unstable_ToolInteractionLog>>(
327
+ new Map(),
328
+ );
266
329
  const lastRunConfigRef = useRef<RunConfig | undefined>(undefined);
330
+ const markToolArtifactsChanged = useCallback(() => {
331
+ setToolArtifactEpoch((epoch) => epoch + 1);
332
+ }, []);
333
+ const markToolInteractionsChanged = useCallback(() => {
334
+ setToolInteractionEpoch((epoch) => epoch + 1);
335
+ }, []);
267
336
 
268
337
  const hasExecutingTools = Object.values(toolStatuses).some(
269
338
  (s) => s?.type === "executing",
@@ -285,6 +354,25 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
285
354
  cancelledMessages?.chatId === chatHelpers.id
286
355
  ? cancelledMessages.ids
287
356
  : NO_CANCELLED_MESSAGE_IDS;
357
+ const supportsRichToolApprovalResponses =
358
+ customOnRespondToToolApproval != null;
359
+
360
+ const toThreadMessages = useCallback(
361
+ (sourceMessages: UI_MESSAGE[]) => {
362
+ const metadata: AISDKMessageConverterMetadata = {
363
+ supportsRichToolApprovalResponses,
364
+ toolArtifacts: toolArtifactsRef.current,
365
+ toolInteractions: toolInteractionsRef.current,
366
+ toolApprovalResponses: toolApprovalResponsesRef.current,
367
+ };
368
+ return AISDKMessageConverter.toThreadMessages(
369
+ sourceMessages,
370
+ false,
371
+ metadata,
372
+ );
373
+ },
374
+ [supportsRichToolApprovalResponses],
375
+ );
288
376
 
289
377
  const retractCancellation = useCallback(
290
378
  (chatId: string, messageId: string) => {
@@ -321,18 +409,23 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
321
409
  isRunning,
322
410
  messages: chatHelpers.messages,
323
411
  joinStrategy,
324
- metadata: useMemo(
412
+ metadata: useMemo<AISDKMessageConverterMetadata>(
325
413
  () => ({
326
414
  toolStatuses,
327
415
  messageTiming,
328
416
  toolArgsKeyOrderCache: toolArgsKeyOrderCacheRef.current,
417
+ toolArgsTextCache: toolArgsTextCacheRef.current,
329
418
  toolLastInputCache: toolLastInputCacheRef.current,
330
419
  mcpAppMetadataCache: mcpAppMetadataCacheRef.current,
420
+ toolArtifacts: toolArtifactsRef.current,
421
+ toolInteractions: toolInteractionsRef.current,
422
+ supportsRichToolApprovalResponses,
331
423
  ...(optimisticMessageId && { optimisticMessageId }),
332
424
  ...(chatHelpers.error && {
333
425
  error: toChatError(chatHelpers.error),
334
426
  }),
335
427
  ...(cancelledMessageIds.size > 0 && { cancelledMessageIds }),
428
+ ...(toolApprovalResponses.size > 0 && { toolApprovalResponses }),
336
429
  }),
337
430
  [
338
431
  toolStatuses,
@@ -340,6 +433,10 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
340
433
  optimisticMessageId,
341
434
  chatHelpers.error,
342
435
  cancelledMessageIds,
436
+ toolApprovalResponses,
437
+ supportsRichToolApprovalResponses,
438
+ toolArtifactEpoch,
439
+ toolInteractionEpoch,
343
440
  ],
344
441
  ),
345
442
  });
@@ -347,13 +444,11 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
347
444
  const exportedMessageRepository = useMemo(() => {
348
445
  if (!messageRepository) return undefined;
349
446
  const converted = toExportedMessageRepository(
350
- AISDKMessageConverter.toThreadMessages as (
351
- messages: UI_MESSAGE[],
352
- ) => ThreadMessage[],
447
+ toThreadMessages,
353
448
  messageRepository,
354
449
  );
355
450
  return converted.messages.length > 0 ? converted : undefined;
356
- }, [messageRepository]);
451
+ }, [messageRepository, toThreadMessages]);
357
452
 
358
453
  const generatedSuggestions = useGeneratedSuggestions(
359
454
  suggestionAdapter,
@@ -367,12 +462,15 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
367
462
  },
368
463
  }));
369
464
 
370
- const { isLoading, deleteMessage: deleteHistoryMessage } = useExternalHistory(
465
+ const {
466
+ isLoading,
467
+ deleteMessage: deleteHistoryMessage,
468
+ persistToolInteractions,
469
+ persistToolApprovalResponses,
470
+ } = useExternalHistory(
371
471
  runtimeRef,
372
472
  adapters?.history ?? contextAdapters?.history,
373
- AISDKMessageConverter.toThreadMessages as (
374
- messages: UI_MESSAGE[],
375
- ) => ThreadMessage[],
473
+ toThreadMessages,
376
474
  aiSDKV6FormatAdapter as MessageFormatAdapter<
377
475
  UI_MESSAGE,
378
476
  AISDKStorageFormat
@@ -380,6 +478,17 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
380
478
  (messages) => {
381
479
  chatHelpers.setMessages(messages);
382
480
  },
481
+ toolArtifactsRef.current,
482
+ markToolArtifactsChanged,
483
+ toolInteractionsRef.current,
484
+ markToolInteractionsChanged,
485
+ toolApprovalResponsesRef.current,
486
+ () => {
487
+ hostApprovalIdsRef.current = new Set(
488
+ toolApprovalResponsesRef.current.keys(),
489
+ );
490
+ setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
491
+ },
383
492
  );
384
493
 
385
494
  const {
@@ -402,38 +511,112 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
402
511
  if (!cancelPendingToolCallsOnSend) return;
403
512
 
404
513
  // The runtime auto-aborts in-flight tool invocations when a new run
405
- // is dispatched (append() / startRun()). All we need to do here is
406
- // mark any tool without a result as cancelled in the UI message list.
407
-
408
- // Mark any tool without a result as cancelled (uses setMessages to avoid triggering sendAutomaticallyWhen)
514
+ // is dispatched (append() / startRun()), so this only has to mark the
515
+ // abandoned tools cancelled in the UI message list. Every non-terminal
516
+ // tool call qualifies wherever it sits: the run that produced it is over,
517
+ // and a staged `startRun: false` message can sit between it and the tail.
518
+ // Uses setMessages to avoid triggering sendAutomaticallyWhen.
409
519
  chatHelpers.setMessages((messages) => {
410
- const lastMessage = messages.at(-1);
411
- if (lastMessage?.role !== "assistant") return messages;
412
-
413
520
  let hasChanges = false;
414
- const parts = lastMessage.parts?.map((part) => {
415
- if (!isToolUIPart(part)) return part;
416
- if (
417
- part.state === "output-available" ||
418
- part.state === "output-error" ||
419
- part.state === "output-denied"
420
- )
421
- return part;
422
521
 
522
+ const next = messages.map((message) => {
523
+ if (message.role !== "assistant") return message;
524
+
525
+ let messageChanged = false;
526
+ const parts = message.parts?.map((part) => {
527
+ if (!isToolUIPart(part)) return part;
528
+ if (
529
+ part.state === "output-available" ||
530
+ part.state === "output-error" ||
531
+ part.state === "output-denied"
532
+ )
533
+ return part;
534
+
535
+ messageChanged = true;
536
+ const { approval: _approval, ...rest } = part;
537
+ return {
538
+ ...rest,
539
+ state: "output-error" as const,
540
+ errorText: "User cancelled tool call by sending a new message.",
541
+ };
542
+ });
543
+
544
+ if (!messageChanged) return message;
423
545
  hasChanges = true;
424
- const { approval: _approval, ...rest } = part;
425
- return {
426
- ...rest,
427
- state: "output-error" as const,
428
- errorText: "User cancelled tool call by sending a new message.",
429
- };
546
+ return { ...message, parts };
430
547
  });
431
548
 
432
549
  if (!hasChanges) return messages;
433
- return [...messages.slice(0, -1), { ...lastMessage, parts }];
550
+ return next;
434
551
  });
435
552
  };
436
553
 
554
+ const respondViaAISDK = ({
555
+ approvalId,
556
+ approved,
557
+ reason,
558
+ }: RespondToToolApprovalOptions) =>
559
+ Promise.resolve(
560
+ chatHelpers.addToolApprovalResponse({
561
+ id: approvalId,
562
+ approved,
563
+ ...(reason != null && { reason }),
564
+ options: { metadata: lastRunConfigRef.current },
565
+ }),
566
+ );
567
+
568
+ const respondViaHost = async (
569
+ onRespond: NonNullable<AISDKRuntimeAdapter["onRespondToToolApproval"]>,
570
+ response: RespondToToolApprovalOptions,
571
+ ) => {
572
+ const { approvalId } = response;
573
+ const requested = chatHelpers.messages
574
+ .flatMap((message) =>
575
+ message.parts.flatMap((part) =>
576
+ isToolUIPart(part) ? [{ messageId: message.id, part }] : [],
577
+ ),
578
+ )
579
+ .find(
580
+ ({ part }) =>
581
+ part.state === "approval-requested" &&
582
+ part.approval.id === approvalId,
583
+ );
584
+ if (!requested || hostApprovalIdsRef.current.has(approvalId))
585
+ throw new Error(
586
+ `Tool approval ${approvalId} is not waiting for a response.`,
587
+ );
588
+
589
+ // A host answer stays out of the useChat messages, where sendAutomaticallyWhen would forward it to the chat route.
590
+ const applyResponse = (applied: boolean) => {
591
+ if (applied) hostApprovalIdsRef.current.add(approvalId);
592
+ else hostApprovalIdsRef.current.delete(approvalId);
593
+ if (applied) toolApprovalResponsesRef.current.set(approvalId, response);
594
+ else toolApprovalResponsesRef.current.delete(approvalId);
595
+ setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
596
+ };
597
+
598
+ applyResponse(true);
599
+ try {
600
+ await onRespond(response, {
601
+ toolCallId: requested.part.toolCallId,
602
+ toolName: getToolName(requested.part),
603
+ respondViaAISDK: async () => {
604
+ try {
605
+ await respondViaAISDK(response);
606
+ } finally {
607
+ applyResponse(false);
608
+ }
609
+ },
610
+ });
611
+ } catch (error) {
612
+ if (hostApprovalIdsRef.current.has(approvalId)) applyResponse(false);
613
+ throw error;
614
+ }
615
+ if (hostApprovalIdsRef.current.has(approvalId)) {
616
+ await persistToolApprovalResponses(requested.messageId);
617
+ }
618
+ };
619
+
437
620
  const hasSeededRepositoryRef = useRef(false);
438
621
  const shouldFeedRepository =
439
622
  exportedMessageRepository != null &&
@@ -441,6 +624,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
441
624
  messages.length === 0;
442
625
 
443
626
  const runtime = useExternalStoreRuntime({
627
+ unstable_persistsHistory: true,
444
628
  isRunning: providerIsRunning,
445
629
  ...(shouldFeedRepository
446
630
  ? { messageRepository: exportedMessageRepository }
@@ -461,6 +645,11 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
461
645
  .filter(Boolean)
462
646
  .flat(),
463
647
  ),
648
+ onVoiceTranscript: (message: ThreadMessage) =>
649
+ chatHelpers.setMessages((current) => [
650
+ ...current,
651
+ toVoiceTranscriptUIMessage<UI_MESSAGE>(message),
652
+ ]),
464
653
  onExportExternalState: (): MessageFormatRepository<UI_MESSAGE> => {
465
654
  const exported = runtimeRef.current.thread.export();
466
655
 
@@ -501,10 +690,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
501
690
  },
502
691
  onLoadExternalState: (repo: MessageFormatRepository<UI_MESSAGE>) => {
503
692
  // Convert MessageFormatRepository to ExportedMessageRepository
504
- const exportedRepo = toExportedMessageRepository(
505
- AISDKMessageConverter.toThreadMessages,
506
- repo,
507
- );
693
+ const exportedRepo = toExportedMessageRepository(toThreadMessages, repo);
508
694
 
509
695
  // Import into the thread's MessageRepository
510
696
  runtimeRef.current.thread.import(exportedRepo);
@@ -584,6 +770,34 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
584
770
 
585
771
  await deleteHistoryMessage(messageId);
586
772
 
773
+ let removedToolArtifact = false;
774
+ let removedToolInteractions = false;
775
+ let removedToolApprovalResponse = false;
776
+ let removedHostApprovalId = false;
777
+ for (const part of threadMessages[messageIndex]!.content) {
778
+ if (part.type === "tool-call") {
779
+ removedToolArtifact =
780
+ toolArtifactsRef.current.delete(part.toolCallId) ||
781
+ removedToolArtifact;
782
+ removedToolInteractions =
783
+ toolInteractionsRef.current.delete(part.toolCallId) ||
784
+ removedToolInteractions;
785
+ if (part.approval) {
786
+ removedToolApprovalResponse =
787
+ toolApprovalResponsesRef.current.delete(part.approval.id) ||
788
+ removedToolApprovalResponse;
789
+ removedHostApprovalId =
790
+ hostApprovalIdsRef.current.delete(part.approval.id) ||
791
+ removedHostApprovalId;
792
+ }
793
+ }
794
+ }
795
+ if (removedToolArtifact) markToolArtifactsChanged();
796
+ if (removedToolInteractions) markToolInteractionsChanged();
797
+ if (removedToolApprovalResponse || removedHostApprovalId) {
798
+ setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
799
+ }
800
+
587
801
  const deleteIds = new Set(
588
802
  getExternalStoreMessages<UI_MESSAGE>(threadMessages[messageIndex]!).map(
589
803
  (message) => message.id,
@@ -605,8 +819,13 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
605
819
  toolName,
606
820
  result,
607
821
  isError,
822
+ artifact,
608
823
  modelContent,
609
824
  }) => {
825
+ if (artifact !== undefined) {
826
+ toolArtifactsRef.current.set(toolCallId, artifact);
827
+ markToolArtifactsChanged();
828
+ }
610
829
  const options = { metadata: lastRunConfigRef.current };
611
830
  if (isError) {
612
831
  return Promise.resolve(
@@ -634,15 +853,24 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
634
853
  );
635
854
  }
636
855
  },
637
- onRespondToToolApproval: ({ approvalId, approved, reason }) =>
638
- Promise.resolve(
639
- chatHelpers.addToolApprovalResponse({
640
- id: approvalId,
641
- approved,
642
- ...(reason != null && { reason }),
643
- options: { metadata: lastRunConfigRef.current },
644
- }),
645
- ),
856
+ onRespondToToolApproval: customOnRespondToToolApproval
857
+ ? (response) => respondViaHost(customOnRespondToToolApproval, response)
858
+ : respondViaAISDK,
859
+ unstable_onRecordToolInteraction: ({
860
+ messageId,
861
+ toolCallId,
862
+ interaction,
863
+ }) => {
864
+ toolInteractionsRef.current.set(
865
+ toolCallId,
866
+ appendToolInteraction(
867
+ toolInteractionsRef.current.get(toolCallId),
868
+ interaction,
869
+ ),
870
+ );
871
+ markToolInteractionsChanged();
872
+ return persistToolInteractions(messageId);
873
+ },
646
874
  ...pickExternalStoreSharedOptions(adapter),
647
875
  ...(adapter.unstable_messageRepositoryInstance && {
648
876
  unstable_messageRepositoryInstance: