@assistant-ui/ai-sdk 0.0.7 → 0.0.9

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 (115) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
  4. package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
  5. package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -3
  6. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
  7. package/dist/adapters/vercelAttachmentAdapter.js +3 -3
  8. package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
  9. package/dist/aiSDKExtras.d.ts +4 -6
  10. package/dist/aiSDKExtras.d.ts.map +1 -1
  11. package/dist/converters/convertMessage.d.ts +34 -24
  12. package/dist/converters/convertMessage.d.ts.map +1 -1
  13. package/dist/converters/convertMessage.js +56 -12
  14. package/dist/converters/convertMessage.js.map +1 -1
  15. package/dist/converters/modelContentEnvelope.d.ts +6 -7
  16. package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
  17. package/dist/converters/toCreateMessage.d.ts +2 -4
  18. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  19. package/dist/converters/toolOutputConversion.d.ts +20 -22
  20. package/dist/converters/toolOutputConversion.d.ts.map +1 -1
  21. package/dist/hooks.d.ts +2 -4
  22. package/dist/hooks.d.ts.map +1 -1
  23. package/dist/index.d.ts +17 -14
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.native.d.ts +15 -12
  26. package/dist/index.native.d.ts.map +1 -0
  27. package/dist/model-context/injectInteractableContext.d.ts +2 -4
  28. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  29. package/dist/model-context/injectQuoteContext.d.ts +1 -3
  30. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  31. package/dist/runtime/AISDKChat.d.ts +2 -4
  32. package/dist/runtime/AISDKChat.d.ts.map +1 -1
  33. package/dist/runtime/AISDKChat.js +1 -1
  34. package/dist/runtime/AISDKChat.js.map +1 -1
  35. package/dist/runtime/AISDKThreads.d.ts +29 -31
  36. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  37. package/dist/runtime/AISDKThreads.js +22 -23
  38. package/dist/runtime/AISDKThreads.js.map +1 -1
  39. package/dist/runtime/sdkIdentity.d.ts +1 -3
  40. package/dist/runtime/sdkIdentity.d.ts.map +1 -1
  41. package/dist/runtime/sdkIdentity.js +1 -1
  42. package/dist/runtime/useAISDKRuntime.d.ts +76 -70
  43. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  44. package/dist/runtime/useAISDKRuntime.js +311 -77
  45. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  46. package/dist/runtime/useChatRuntime.d.ts +6 -8
  47. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  48. package/dist/runtime/useChatRuntime.js +14 -10
  49. package/dist/runtime/useChatRuntime.js.map +1 -1
  50. package/dist/runtime/useChatThread.d.ts +94 -89
  51. package/dist/runtime/useChatThread.d.ts.map +1 -1
  52. package/dist/runtime/useChatThread.js +44 -15
  53. package/dist/runtime/useChatThread.js.map +1 -1
  54. package/dist/runtime/useExternalHistory.d.ts +7 -7
  55. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  56. package/dist/runtime/useExternalHistory.js +231 -60
  57. package/dist/runtime/useExternalHistory.js.map +1 -1
  58. package/dist/runtime/useResourceCleanup.d.ts +1 -3
  59. package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
  60. package/dist/runtime/useResourceCleanup.js +38 -14
  61. package/dist/runtime/useResourceCleanup.js.map +1 -1
  62. package/dist/runtime/useStreamingTiming.d.ts +3 -5
  63. package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
  64. package/dist/runtime/useStreamingTiming.js +1 -1
  65. package/dist/tools/frontendTools.d.ts +21 -23
  66. package/dist/tools/frontendTools.d.ts.map +1 -1
  67. package/dist/tools/generativeTools.d.ts +24 -26
  68. package/dist/tools/generativeTools.d.ts.map +1 -1
  69. package/dist/tools/mcp-stdio.node.d.ts +2 -2
  70. package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
  71. package/dist/tools/mcp-stdio.unsupported.d.ts +2 -4
  72. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
  73. package/dist/transport/AssistantChatTransport.d.ts +14 -16
  74. package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
  75. package/dist/transport/AssistantChatTransport.js +9 -2
  76. package/dist/transport/AssistantChatTransport.js.map +1 -1
  77. package/dist/transport/resumable.d.ts +25 -27
  78. package/dist/transport/resumable.d.ts.map +1 -1
  79. package/dist/usage.d.ts +8 -10
  80. package/dist/usage.d.ts.map +1 -1
  81. package/dist/usage.js.map +1 -1
  82. package/dist/utils/getVercelAIMessages.d.ts +2 -4
  83. package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
  84. package/dist/utils/sliceMessagesUntil.d.ts +1 -3
  85. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  86. package/package.json +13 -9
  87. package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
  88. package/src/adapters/vercelAttachmentAdapter.ts +2 -2
  89. package/src/converters/convertMessage.test.ts +541 -3
  90. package/src/converters/convertMessage.ts +123 -15
  91. package/src/index.native.ts +1 -1
  92. package/src/index.ts +1 -1
  93. package/src/runtime/AISDKChat.ts +0 -4
  94. package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
  95. package/src/runtime/AISDKThreads.test.ts +105 -46
  96. package/src/runtime/AISDKThreads.ts +1 -1
  97. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1639 -19
  98. package/src/runtime/useAISDKRuntime.approval.test.tsx +252 -9
  99. package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
  100. package/src/runtime/useAISDKRuntime.test.ts +275 -5
  101. package/src/runtime/useAISDKRuntime.ts +533 -60
  102. package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
  103. package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
  104. package/src/runtime/useChatRuntime.integration.test.tsx +143 -65
  105. package/src/runtime/useChatRuntime.test.ts +11 -7
  106. package/src/runtime/useChatRuntime.ts +6 -2
  107. package/src/runtime/useChatThread.test.ts +166 -2
  108. package/src/runtime/useChatThread.transport.test.tsx +152 -0
  109. package/src/runtime/useChatThread.ts +89 -22
  110. package/src/runtime/useExternalHistory.test.ts +943 -3
  111. package/src/runtime/useExternalHistory.ts +378 -15
  112. package/src/runtime/useResourceCleanup.ts +41 -15
  113. package/src/transport/AssistantChatTransport.test.ts +164 -0
  114. package/src/transport/AssistantChatTransport.ts +22 -2
  115. package/src/usage.ts +1 -1
@@ -1,20 +1,21 @@
1
1
  "use client";
2
- import { aiSDKExtras } from "../aiSDKExtras.js";
3
- import { sliceMessagesUntil } from "../utils/sliceMessagesUntil.js";
4
- import { toCreateMessage } from "../converters/toCreateMessage.js";
2
+ import { aiSDKV6FormatAdapter } from "../adapters/aiSDKFormatAdapter.js";
5
3
  import { vercelAttachmentAdapter } from "../adapters/vercelAttachmentAdapter.js";
6
- import { getVercelAIMessages } from "../utils/getVercelAIMessages.js";
4
+ import { aiSDKExtras } from "../aiSDKExtras.js";
7
5
  import { wrapModelContentEnvelope } from "../converters/modelContentEnvelope.js";
8
6
  import { AISDKMessageConverter } from "../converters/convertMessage.js";
9
- import { aiSDKV6FormatAdapter } from "../adapters/aiSDKFormatAdapter.js";
7
+ import { toCreateMessage } from "../converters/toCreateMessage.js";
8
+ import { sliceMessagesUntil } from "../utils/sliceMessagesUntil.js";
9
+ import { getVercelAIMessages } from "../utils/getVercelAIMessages.js";
10
10
  import { toExportedMessageRepository, useExternalHistory } from "./useExternalHistory.js";
11
11
  import { useStreamingTiming as useStreamingTiming$1 } from "./useStreamingTiming.js";
12
+ import { MessageRepository, appendToolInteraction, consumeSuggestionResult } from "@assistant-ui/core/internal";
13
+ import { generateId, getToolName, isToolUIPart } from "ai";
12
14
  import { useExternalStoreRuntime, useRuntimeAdapters } from "@assistant-ui/core/react";
15
+ import { getExternalStoreMessages, pickExternalStoreSharedOptions } from "@assistant-ui/core";
13
16
  import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
14
17
  import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from "@assistant-ui/tap/react-shim";
15
- import { generateId, getToolName, isToolUIPart } from "ai";
16
- import { getExternalStoreMessages, pickExternalStoreSharedOptions } from "@assistant-ui/core";
17
- import { MessageRepository, consumeSuggestionResult } from "@assistant-ui/core/internal";
18
+ import { useReplaySafeEffect } from "@assistant-ui/store/internal";
18
19
  //#region src/runtime/useAISDKRuntime.ts
19
20
  const toUIMessage = (createMessage, fallbackRole) => ({
20
21
  ...createMessage,
@@ -29,7 +30,7 @@ const toVoiceTranscriptUIMessage = (message) => ({
29
30
  text: part.text
30
31
  })),
31
32
  metadata: {
32
- modality: "voice",
33
+ ...message.metadata.modality && { modality: message.metadata.modality },
33
34
  ...Object.keys(message.metadata.custom).length > 0 && { custom: message.metadata.custom }
34
35
  }
35
36
  });
@@ -137,11 +138,97 @@ const useGeneratedSuggestions = (suggestionAdapter, messages, isRunning) => {
137
138
  t6 = $[11];
138
139
  t7 = $[12];
139
140
  }
140
- useEffect(t6, t7);
141
+ useReplaySafeEffect(t6, t7);
141
142
  return suggestions;
142
143
  };
143
144
  const NO_CANCELLED_MESSAGE_IDS = /* @__PURE__ */ new Set();
145
+ const NO_SUPERSEDED_APPROVAL_PROJECTION = Object.freeze({
146
+ approvalIds: Object.freeze(/* @__PURE__ */ new Set()),
147
+ statusMessageIds: Object.freeze(/* @__PURE__ */ new Set())
148
+ });
144
149
  const NO_TOOL_APPROVAL_RESPONSES = /* @__PURE__ */ new Map();
150
+ const hostToolApprovalsByChat = /* @__PURE__ */ new WeakMap();
151
+ const toApprovalResponses = (owned) => owned && owned.size > 0 ? new Map([...owned].map(([id, entry]) => [id, entry.response])) : NO_TOOL_APPROVAL_RESPONSES;
152
+ /**
153
+ * The answers live on the owner, but each mounted runtime renders them from
154
+ * its own state, so a write has to be announced: the runtime that performed it
155
+ * may already be unmounted (a rollback resolving after a remount), and another
156
+ * runtime may be mounted over the same owner.
157
+ */
158
+ const hostApprovalListenersByChat = /* @__PURE__ */ new WeakMap();
159
+ const subscribeToHostApprovals = (owner, listener) => {
160
+ const listeners = hostApprovalListenersByChat.get(owner) ?? /* @__PURE__ */ new Set();
161
+ hostApprovalListenersByChat.set(owner, listeners);
162
+ listeners.add(listener);
163
+ return () => {
164
+ listeners.delete(listener);
165
+ };
166
+ };
167
+ const notifyHostApprovals = (owner) => {
168
+ for (const listener of [...hostApprovalListenersByChat.get(owner) ?? []]) listener();
169
+ };
170
+ const getSupersededApprovalProjection = (messages, hostApprovalIds, joinStrategy, isRunning) => {
171
+ const approvalIds = /* @__PURE__ */ new Set();
172
+ const statusMessageIds = /* @__PURE__ */ new Set();
173
+ const lastIndex = messages.length - 1;
174
+ let lastAssistant;
175
+ let hasSupersededApproval = false;
176
+ let previousWasVoice = false;
177
+ const flush = () => {
178
+ const hasOpenToolPart = lastAssistant === messages[lastIndex] && lastAssistant?.parts?.some((part) => {
179
+ if (!isToolUIPart(part) || part.state === "output-available" || part.state === "output-error" || part.state === "output-denied") return false;
180
+ const approval = part.approval;
181
+ const resolution = approval?.resolution ?? (approval?.descriptor)?.resolution;
182
+ return resolution !== "cancelled" && resolution !== "expired";
183
+ });
184
+ if (lastAssistant && hasSupersededApproval && !hasOpenToolPart && !(isRunning && lastAssistant === messages[lastIndex])) statusMessageIds.add(lastAssistant.id);
185
+ lastAssistant = void 0;
186
+ hasSupersededApproval = false;
187
+ previousWasVoice = false;
188
+ };
189
+ for (let index = 0; index < messages.length; index++) {
190
+ const message = messages[index];
191
+ if (message.role !== "assistant") flush();
192
+ else {
193
+ const isVoice = message.metadata?.modality === "voice";
194
+ if (isVoice || previousWasVoice || joinStrategy === "none") flush();
195
+ lastAssistant = message;
196
+ previousWasVoice = isVoice;
197
+ }
198
+ for (const part of message.parts ?? []) {
199
+ if (!isToolUIPart(part) || part.state !== "approval-requested") continue;
200
+ const approval = part.approval;
201
+ if (!approval) continue;
202
+ const approvalId = approval.id;
203
+ if (hostApprovalIds.has(approvalId)) continue;
204
+ const resolution = approval.resolution ?? approval.descriptor?.resolution;
205
+ if (resolution === "cancelled" || resolution === "expired") continue;
206
+ if (index !== lastIndex) {
207
+ approvalIds.add(approvalId);
208
+ if (message.role === "assistant") hasSupersededApproval = true;
209
+ }
210
+ }
211
+ }
212
+ flush();
213
+ if (approvalIds.size === 0 && statusMessageIds.size === 0) return NO_SUPERSEDED_APPROVAL_PROJECTION;
214
+ return {
215
+ approvalIds,
216
+ statusMessageIds
217
+ };
218
+ };
219
+ const findRawToolMessageIndex = (messages, messageId, toolCallId, joinStrategy) => {
220
+ const containsToolCall = (message) => message.parts?.some((part) => isToolUIPart(part) && part.toolCallId === toolCallId) === true;
221
+ const messageIndex = messages.findIndex((message) => message.id === messageId);
222
+ if (messageIndex === -1) return -1;
223
+ if (containsToolCall(messages[messageIndex])) return messageIndex;
224
+ if (joinStrategy === "none" || messages[messageIndex]?.role !== "assistant") return -1;
225
+ let start = messageIndex;
226
+ while (start > 0 && messages[start - 1]?.role === "assistant") start--;
227
+ let end = messageIndex;
228
+ while (end + 1 < messages.length && messages[end + 1]?.role === "assistant") end++;
229
+ for (let index = start; index <= end; index++) if (containsToolCall(messages[index])) return index;
230
+ return -1;
231
+ };
145
232
  const toChatError = (error) => {
146
233
  const code = error.code;
147
234
  return {
@@ -155,12 +242,55 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
155
242
  const contextAdapters = useRuntimeAdapters();
156
243
  const [toolStatuses, setToolStatuses] = useState({});
157
244
  const [cancelledMessages, setCancelledMessages] = useState(null);
158
- const [toolApprovalResponses, setToolApprovalResponses] = useState(NO_TOOL_APPROVAL_RESPONSES);
159
- const hostApprovalIdsRef = useRef(/* @__PURE__ */ new Set());
245
+ const approvalOwner = adapter.unstable_hostApprovalOwner;
246
+ const ownedApprovals = approvalOwner ? hostToolApprovalsByChat.get(approvalOwner) ?? (() => {
247
+ const created = /* @__PURE__ */ new Map();
248
+ hostToolApprovalsByChat.set(approvalOwner, created);
249
+ return created;
250
+ })() : void 0;
251
+ const [toolApprovalResponses, setToolApprovalResponses] = useState(() => toApprovalResponses(ownedApprovals));
252
+ const [toolArtifactEpoch, setToolArtifactEpoch] = useState(0);
253
+ const [toolInteractionEpoch, setToolInteractionEpoch] = useState(0);
254
+ const toolApprovalResponsesRef = useRef(new Map(toApprovalResponses(ownedApprovals)));
255
+ const ownedApprovalIdsRef = useRef(new Set(ownedApprovals?.keys()));
256
+ const hostApprovalIdsRef = useRef(new Set(ownedApprovals?.keys()));
257
+ useEffect(() => {
258
+ if (!approvalOwner || !ownedApprovals) return void 0;
259
+ const sync = () => {
260
+ for (const id of ownedApprovalIdsRef.current) toolApprovalResponsesRef.current.delete(id);
261
+ for (const [id_0, entry] of ownedApprovals) toolApprovalResponsesRef.current.set(id_0, entry.response);
262
+ ownedApprovalIdsRef.current = new Set(ownedApprovals.keys());
263
+ hostApprovalIdsRef.current = new Set(toolApprovalResponsesRef.current.keys());
264
+ setToolApprovalResponses((prev) => {
265
+ const next = new Map(toolApprovalResponsesRef.current);
266
+ return prev.size === next.size && [...next].every(([id_1, response]) => prev.get(id_1) === response) ? prev : next;
267
+ });
268
+ };
269
+ const unsubscribe = subscribeToHostApprovals(approvalOwner, sync);
270
+ sync();
271
+ return unsubscribe;
272
+ }, [approvalOwner, ownedApprovals]);
273
+ const lastApprovalOwnerRef = useRef(approvalOwner);
274
+ if (lastApprovalOwnerRef.current !== approvalOwner) {
275
+ lastApprovalOwnerRef.current = approvalOwner;
276
+ hostApprovalIdsRef.current = new Set(ownedApprovals?.keys());
277
+ toolApprovalResponsesRef.current = new Map(toApprovalResponses(ownedApprovals));
278
+ ownedApprovalIdsRef.current = new Set(ownedApprovals?.keys());
279
+ setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
280
+ }
160
281
  const toolArgsKeyOrderCacheRef = useRef(/* @__PURE__ */ new Map());
161
282
  const toolLastInputCacheRef = useRef(/* @__PURE__ */ new Map());
283
+ const toolArgsTextCacheRef = useRef(/* @__PURE__ */ new WeakMap());
162
284
  const mcpAppMetadataCacheRef = useRef(/* @__PURE__ */ new Map());
285
+ const toolArtifactsRef = useRef(/* @__PURE__ */ new Map());
286
+ const toolInteractionsRef = useRef(/* @__PURE__ */ new Map());
163
287
  const lastRunConfigRef = useRef(void 0);
288
+ const markToolArtifactsChanged = useCallback(() => {
289
+ setToolArtifactEpoch((epoch) => epoch + 1);
290
+ }, []);
291
+ const markToolInteractionsChanged = useCallback(() => {
292
+ setToolInteractionEpoch((epoch_0) => epoch_0 + 1);
293
+ }, []);
164
294
  const hasExecutingTools = Object.values(toolStatuses).some((s) => s?.type === "executing");
165
295
  const providerIsRunning = chatHelpers.status === "submitted" || chatHelpers.status === "streaming";
166
296
  const isRunning = providerIsRunning || hasExecutingTools;
@@ -170,14 +300,32 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
170
300
  const optimisticMessageId = isRunning && lastMessage?.role === "assistant" ? lastMessage.id : void 0;
171
301
  const cancelledMessageIds = cancelledMessages?.chatId === chatHelpers.id ? cancelledMessages.ids : NO_CANCELLED_MESSAGE_IDS;
172
302
  const supportsRichToolApprovalResponses = customOnRespondToToolApproval != null;
303
+ const supersededApprovalProjectionRef = useRef(NO_SUPERSEDED_APPROVAL_PROJECTION);
304
+ const supersededApprovalProjection = useMemo(() => {
305
+ const projection = getSupersededApprovalProjection(chatHelpers.messages, hostApprovalIdsRef.current, joinStrategy, isRunning);
306
+ const previous = supersededApprovalProjectionRef.current;
307
+ if (projection.approvalIds.size === previous.approvalIds.size && projection.statusMessageIds.size === previous.statusMessageIds.size && [...projection.approvalIds].every((id_3) => previous.approvalIds.has(id_3)) && [...projection.statusMessageIds].every((id_2) => previous.statusMessageIds.has(id_2))) return previous;
308
+ supersededApprovalProjectionRef.current = projection;
309
+ return projection;
310
+ }, [
311
+ chatHelpers.messages,
312
+ joinStrategy,
313
+ isRunning,
314
+ toolApprovalResponses
315
+ ]);
173
316
  const toThreadMessages = useCallback((sourceMessages) => {
174
- const metadata = { supportsRichToolApprovalResponses };
317
+ const metadata = {
318
+ supportsRichToolApprovalResponses,
319
+ toolArtifacts: toolArtifactsRef.current,
320
+ toolInteractions: toolInteractionsRef.current,
321
+ toolApprovalResponses: toolApprovalResponsesRef.current
322
+ };
175
323
  return AISDKMessageConverter.toThreadMessages(sourceMessages, false, metadata);
176
324
  }, [supportsRichToolApprovalResponses]);
177
325
  const retractCancellation = useCallback((chatId, messageId) => {
178
- setCancelledMessages((prev) => {
179
- if (prev?.chatId !== chatId || !prev.ids.has(messageId)) return prev;
180
- const ids = new Set(prev.ids);
326
+ setCancelledMessages((prev_0) => {
327
+ if (prev_0?.chatId !== chatId || !prev_0.ids.has(messageId)) return prev_0;
328
+ const ids = new Set(prev_0.ids);
181
329
  ids.delete(messageId);
182
330
  return {
183
331
  chatId,
@@ -205,9 +353,14 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
205
353
  toolStatuses,
206
354
  messageTiming,
207
355
  toolArgsKeyOrderCache: toolArgsKeyOrderCacheRef.current,
356
+ toolArgsTextCache: toolArgsTextCacheRef.current,
208
357
  toolLastInputCache: toolLastInputCacheRef.current,
209
358
  mcpAppMetadataCache: mcpAppMetadataCacheRef.current,
359
+ toolArtifacts: toolArtifactsRef.current,
360
+ toolInteractions: toolInteractionsRef.current,
210
361
  supportsRichToolApprovalResponses,
362
+ cancelledToolApprovalIds: supersededApprovalProjection.approvalIds,
363
+ cancelledStatusMessageIds: supersededApprovalProjection.statusMessageIds,
211
364
  ...optimisticMessageId && { optimisticMessageId },
212
365
  ...chatHelpers.error && { error: toChatError(chatHelpers.error) },
213
366
  ...cancelledMessageIds.size > 0 && { cancelledMessageIds },
@@ -219,7 +372,10 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
219
372
  chatHelpers.error,
220
373
  cancelledMessageIds,
221
374
  toolApprovalResponses,
222
- supportsRichToolApprovalResponses
375
+ supportsRichToolApprovalResponses,
376
+ supersededApprovalProjection,
377
+ toolArtifactEpoch,
378
+ toolInteractionEpoch
223
379
  ])
224
380
  });
225
381
  const exportedMessageRepository = useMemo(() => {
@@ -231,8 +387,12 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
231
387
  const [runtimeRef] = useState(() => ({ get current() {
232
388
  return runtime;
233
389
  } }));
234
- const { isLoading, deleteMessage: deleteHistoryMessage } = useExternalHistory(runtimeRef, adapters?.history ?? contextAdapters?.history, toThreadMessages, aiSDKV6FormatAdapter, (messages_0) => {
390
+ const { isLoading, deleteMessage: deleteHistoryMessage, persistToolInteractions, persistToolApprovalResponses } = useExternalHistory(runtimeRef, adapters?.history ?? contextAdapters?.history, toThreadMessages, aiSDKV6FormatAdapter, (messages_0) => {
235
391
  chatHelpers.setMessages(messages_0);
392
+ }, toolArtifactsRef.current, markToolArtifactsChanged, toolInteractionsRef.current, markToolInteractionsChanged, toolApprovalResponsesRef.current, () => {
393
+ for (const [id_4, entry_0] of ownedApprovals ?? []) toolApprovalResponsesRef.current.set(id_4, entry_0.response);
394
+ hostApprovalIdsRef.current = new Set(toolApprovalResponsesRef.current.keys());
395
+ setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
236
396
  });
237
397
  const { id: chatId_0, messages: chatMessages, status: chatStatus, error } = chatHelpers;
238
398
  const extras = useMemo(() => aiSDKExtras.provide({
@@ -247,25 +407,30 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
247
407
  const completePendingToolCalls = async () => {
248
408
  if (!cancelPendingToolCallsOnSend) return;
249
409
  chatHelpers.setMessages((messages_1) => {
250
- const lastMessage_0 = messages_1.at(-1);
251
- if (lastMessage_0?.role !== "assistant") return messages_1;
252
410
  let hasChanges = false;
253
- const parts = lastMessage_0.parts?.map((part) => {
254
- if (!isToolUIPart(part)) return part;
255
- if (part.state === "output-available" || part.state === "output-error" || part.state === "output-denied") return part;
411
+ const next_0 = messages_1.map((message) => {
412
+ if (message.role !== "assistant") return message;
413
+ let messageChanged = false;
414
+ const parts = message.parts?.map((part) => {
415
+ if (!isToolUIPart(part)) return part;
416
+ if (part.state === "output-available" || part.state === "output-error" || part.state === "output-denied") return part;
417
+ messageChanged = true;
418
+ const { approval: _approval, ...rest } = part;
419
+ return {
420
+ ...rest,
421
+ state: "output-error",
422
+ errorText: "User cancelled tool call by sending a new message."
423
+ };
424
+ });
425
+ if (!messageChanged) return message;
256
426
  hasChanges = true;
257
- const { approval: _approval, ...rest } = part;
258
427
  return {
259
- ...rest,
260
- state: "output-error",
261
- errorText: "User cancelled tool call by sending a new message."
428
+ ...message,
429
+ parts
262
430
  };
263
431
  });
264
432
  if (!hasChanges) return messages_1;
265
- return [...messages_1.slice(0, -1), {
266
- ...lastMessage_0,
267
- parts
268
- }];
433
+ return next_0;
269
434
  });
270
435
  };
271
436
  const respondViaAISDK = ({ approvalId, approved, reason }) => Promise.resolve(chatHelpers.addToolApprovalResponse({
@@ -274,48 +439,63 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
274
439
  ...reason != null && { reason },
275
440
  options: { metadata: lastRunConfigRef.current }
276
441
  }));
277
- const respondViaHost = async (onRespond, response) => {
278
- const { approvalId: approvalId_0 } = response;
279
- const requested = chatHelpers.messages.flatMap((message) => message.parts).filter(isToolUIPart).find((part_0) => part_0.state === "approval-requested" && part_0.approval.id === approvalId_0);
442
+ const respondViaHost = async (onRespond, response_0) => {
443
+ const { approvalId: approvalId_0 } = response_0;
444
+ const requested = chatHelpers.messages.flatMap((message_0) => message_0.parts.flatMap((part_0) => isToolUIPart(part_0) ? [{
445
+ messageId: message_0.id,
446
+ part: part_0
447
+ }] : [])).find(({ part: part_1 }) => part_1.state === "approval-requested" && part_1.approval?.id === approvalId_0);
280
448
  if (!requested || hostApprovalIdsRef.current.has(approvalId_0)) throw new Error(`Tool approval ${approvalId_0} is not waiting for a response.`);
449
+ const startedWith = ownedApprovals;
450
+ const startedOwner = approvalOwner;
451
+ const startedResponses = toolApprovalResponsesRef.current;
452
+ let isApplied = false;
281
453
  const applyResponse = (applied) => {
454
+ isApplied = applied;
455
+ if (applied) startedWith?.set(approvalId_0, { response: response_0 });
456
+ else startedWith?.delete(approvalId_0);
457
+ if (startedOwner) {
458
+ notifyHostApprovals(startedOwner);
459
+ return;
460
+ }
461
+ if (lastApprovalOwnerRef.current !== startedOwner) return;
282
462
  if (applied) hostApprovalIdsRef.current.add(approvalId_0);
283
463
  else hostApprovalIdsRef.current.delete(approvalId_0);
284
- setToolApprovalResponses((prev_0) => {
285
- const responses = new Map(prev_0);
286
- if (applied) responses.set(approvalId_0, response);
287
- else responses.delete(approvalId_0);
288
- return responses;
289
- });
464
+ if (applied) toolApprovalResponsesRef.current.set(approvalId_0, response_0);
465
+ else toolApprovalResponsesRef.current.delete(approvalId_0);
466
+ setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
290
467
  };
291
468
  applyResponse(true);
292
469
  try {
293
- await onRespond(response, {
294
- toolCallId: requested.toolCallId,
295
- toolName: getToolName(requested),
470
+ await onRespond(response_0, {
471
+ toolCallId: requested.part.toolCallId,
472
+ toolName: getToolName(requested.part),
296
473
  respondViaAISDK: async () => {
297
474
  try {
298
- await respondViaAISDK(response);
475
+ await respondViaAISDK(response_0);
299
476
  } finally {
300
477
  applyResponse(false);
301
478
  }
302
479
  }
303
480
  });
304
481
  } catch (error_0) {
305
- if (hostApprovalIdsRef.current.has(approvalId_0)) applyResponse(false);
482
+ if (isApplied) applyResponse(false);
306
483
  throw error_0;
307
484
  }
485
+ const remainsApplied = startedWith?.has(approvalId_0) ?? startedResponses.has(approvalId_0);
486
+ if (isApplied && remainsApplied) await persistToolApprovalResponses(requested.messageId);
308
487
  };
309
488
  const hasSeededRepositoryRef = useRef(false);
310
489
  const shouldFeedRepository = exportedMessageRepository != null && !hasSeededRepositoryRef.current && messages.length === 0;
311
490
  const runtime = useExternalStoreRuntime({
491
+ unstable_persistsHistory: true,
312
492
  isRunning: providerIsRunning,
313
493
  ...shouldFeedRepository ? { messageRepository: exportedMessageRepository } : { messages },
314
494
  unstable_enableToolInvocations: true,
315
495
  setToolStatuses,
316
496
  setMessages: (messages_2) => chatHelpers.setMessages(messages_2.map(getVercelAIMessages).filter(Boolean).flat()),
317
497
  onImport: (messages_3) => chatHelpers.setMessages(messages_3.map(getVercelAIMessages).filter(Boolean).flat()),
318
- onVoiceTranscript: (message_0) => chatHelpers.setMessages((current) => [...current, toVoiceTranscriptUIMessage(message_0)]),
498
+ onVoiceTranscript: (message_1) => chatHelpers.setMessages((current) => [...current, toVoiceTranscriptUIMessage(message_1)]),
319
499
  onExportExternalState: () => {
320
500
  const exported = runtimeRef.current.thread.export();
321
501
  const expandedMessages = [];
@@ -341,12 +521,12 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
341
521
  runtimeRef.current.thread.import(exportedRepo);
342
522
  },
343
523
  onCancel: async () => {
344
- const message_1 = chatHelpers.messages.at(-1);
345
- const cancelledId = isRunning && message_1?.role === "assistant" ? message_1.id : void 0;
524
+ const message_2 = chatHelpers.messages.at(-1);
525
+ const cancelledId = isRunning && message_2?.role === "assistant" ? message_2.id : void 0;
346
526
  if (cancelledId) {
347
527
  const liveIds = new Set(chatHelpers.messages.map((m) => m.id));
348
528
  setCancelledMessages((prev_1) => {
349
- const kept = prev_1?.chatId === chatHelpers.id ? [...prev_1.ids].filter((id) => liveIds.has(id)) : [];
529
+ const kept = prev_1?.chatId === chatHelpers.id ? [...prev_1.ids].filter((id_5) => liveIds.has(id_5)) : [];
350
530
  return {
351
531
  chatId: chatHelpers.id,
352
532
  ids: /* @__PURE__ */ new Set([...kept, cancelledId])
@@ -362,33 +542,52 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
362
542
  }
363
543
  }
364
544
  },
365
- onNew: async (message_2) => {
366
- const createMessage = (customToCreateMessage ?? toCreateMessage)(message_2);
367
- if (!(message_2.startRun ?? message_2.role === "user")) {
368
- chatHelpers.setMessages((current_0) => [...current_0, toUIMessage(createMessage, message_2.role)]);
545
+ onNew: async (message_3) => {
546
+ const createMessage = (customToCreateMessage ?? toCreateMessage)(message_3);
547
+ if (!(message_3.startRun ?? message_3.role === "user")) {
548
+ chatHelpers.setMessages((current_0) => [...current_0, toUIMessage(createMessage, message_3.role)]);
369
549
  return;
370
550
  }
371
- lastRunConfigRef.current = message_2.runConfig;
551
+ lastRunConfigRef.current = message_3.runConfig;
372
552
  await completePendingToolCalls();
373
- await chatHelpers.sendMessage(createMessage, { metadata: message_2.runConfig });
553
+ await chatHelpers.sendMessage(createMessage, { metadata: message_3.runConfig });
374
554
  },
375
- onEdit: async (message_3) => {
376
- const createMessage_0 = (customToCreateMessage ?? toCreateMessage)(message_3);
377
- if (!(message_3.startRun ?? message_3.role === "user")) {
378
- chatHelpers.setMessages((current_1) => [...sliceMessagesUntil(current_1, message_3.parentId), toUIMessage(createMessage_0, message_3.role)]);
555
+ onEdit: async (message_4) => {
556
+ const createMessage_0 = (customToCreateMessage ?? toCreateMessage)(message_4);
557
+ if (!(message_4.startRun ?? message_4.role === "user")) {
558
+ chatHelpers.setMessages((current_1) => [...sliceMessagesUntil(current_1, message_4.parentId), toUIMessage(createMessage_0, message_4.role)]);
379
559
  return;
380
560
  }
381
- lastRunConfigRef.current = message_3.runConfig;
382
- chatHelpers.setMessages((current_2) => sliceMessagesUntil(current_2, message_3.parentId));
383
- await chatHelpers.sendMessage(createMessage_0, { metadata: message_3.runConfig });
561
+ lastRunConfigRef.current = message_4.runConfig;
562
+ chatHelpers.setMessages((current_2) => sliceMessagesUntil(current_2, message_4.parentId));
563
+ await chatHelpers.sendMessage(createMessage_0, { metadata: message_4.runConfig });
384
564
  },
385
565
  onDelete: async (messageId_0) => {
386
566
  const threadMessages = runtimeRef.current.thread.getState().messages;
387
- const messageIndex = threadMessages.findIndex((message_4) => message_4.id === messageId_0);
567
+ const messageIndex = threadMessages.findIndex((message_5) => message_5.id === messageId_0);
388
568
  if (messageIndex === -1) return;
389
569
  await deleteHistoryMessage(messageId_0);
390
- const deleteIds = new Set(getExternalStoreMessages(threadMessages[messageIndex]).map((message_5) => message_5.id));
391
- chatHelpers.setMessages((current_3) => current_3.filter((message_6) => !deleteIds.has(message_6.id)));
570
+ let removedToolArtifact = false;
571
+ let removedToolInteractions = false;
572
+ let removedToolApprovalResponse = false;
573
+ let removedHostApprovalId = false;
574
+ for (const part_2 of threadMessages[messageIndex].content) if (part_2.type === "tool-call") {
575
+ removedToolArtifact = toolArtifactsRef.current.delete(part_2.toolCallId) || removedToolArtifact;
576
+ removedToolInteractions = toolInteractionsRef.current.delete(part_2.toolCallId) || removedToolInteractions;
577
+ if (part_2.approval) {
578
+ ownedApprovals?.delete(part_2.approval.id);
579
+ removedToolApprovalResponse = toolApprovalResponsesRef.current.delete(part_2.approval.id) || removedToolApprovalResponse;
580
+ removedHostApprovalId = hostApprovalIdsRef.current.delete(part_2.approval.id) || removedHostApprovalId;
581
+ }
582
+ }
583
+ if (removedToolArtifact) markToolArtifactsChanged();
584
+ if (removedToolInteractions) markToolInteractionsChanged();
585
+ if (removedToolApprovalResponse || removedHostApprovalId) {
586
+ if (approvalOwner) notifyHostApprovals(approvalOwner);
587
+ setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
588
+ }
589
+ const deleteIds = new Set(getExternalStoreMessages(threadMessages[messageIndex]).map((message_6) => message_6.id));
590
+ chatHelpers.setMessages((current_3) => current_3.filter((message_7) => !deleteIds.has(message_7.id)));
392
591
  },
393
592
  onReload: async (parentId_0, config) => {
394
593
  lastRunConfigRef.current = config.runConfig;
@@ -396,26 +595,61 @@ const useAISDKRuntime = (chatHelpers, adapter = {}) => {
396
595
  chatHelpers.setMessages(newMessages);
397
596
  await chatHelpers.regenerate({ metadata: config.runConfig });
398
597
  },
399
- onAddToolResult: ({ toolCallId, toolName, result: result_0, isError, modelContent }) => {
598
+ onAddToolResult: ({ messageId: messageId_1, toolCallId, toolName, result: result_0, isError, artifact, modelContent }) => {
599
+ if (artifact !== void 0) {
600
+ toolArtifactsRef.current.set(toolCallId, artifact);
601
+ markToolArtifactsChanged();
602
+ }
603
+ const targetIndex = findRawToolMessageIndex(chatHelpers.messages, messageId_1, toolCallId, joinStrategy);
604
+ const errorText = typeof result_0 === "string" ? result_0 : JSON.stringify(result_0);
605
+ const output = !isError && modelContent !== void 0 ? wrapModelContentEnvelope(result_0, modelContent) : result_0;
606
+ if (targetIndex >= 0 && targetIndex !== chatHelpers.messages.length - 1) {
607
+ const target = chatHelpers.messages[targetIndex];
608
+ const targetPart = target.parts.find((part_3) => isToolUIPart(part_3) && part_3.toolCallId === toolCallId);
609
+ if (targetPart?.state === "output-error" || targetPart?.state === "output-denied" || targetPart?.state === "output-available" && !targetPart.preliminary) return Promise.resolve();
610
+ const targetMessageId = target.id;
611
+ chatHelpers.setMessages((current_4) => current_4.map((message_8) => {
612
+ if (message_8.id !== targetMessageId) return message_8;
613
+ return {
614
+ ...message_8,
615
+ parts: message_8.parts.map((part_4) => {
616
+ if (!isToolUIPart(part_4) || part_4.toolCallId !== toolCallId) return part_4;
617
+ const { preliminary: _preliminary, ...finalPart } = part_4;
618
+ return {
619
+ ...finalPart,
620
+ state: isError ? "output-error" : "output-available",
621
+ output: isError ? void 0 : output,
622
+ errorText: isError ? errorText : void 0
623
+ };
624
+ })
625
+ };
626
+ }));
627
+ return Promise.resolve();
628
+ }
400
629
  const options = { metadata: lastRunConfigRef.current };
401
630
  if (isError) return Promise.resolve(chatHelpers.addToolOutput({
402
631
  state: "output-error",
403
632
  tool: toolName ?? toolCallId,
404
633
  toolCallId,
405
- errorText: typeof result_0 === "string" ? result_0 : JSON.stringify(result_0),
634
+ errorText,
406
635
  options
407
636
  }));
408
- else {
409
- const output = modelContent !== void 0 ? wrapModelContentEnvelope(result_0, modelContent) : result_0;
410
- return Promise.resolve(chatHelpers.addToolOutput({
411
- tool: toolName,
412
- toolCallId,
413
- output,
414
- options
415
- }));
416
- }
637
+ return Promise.resolve(chatHelpers.addToolOutput({
638
+ tool: toolName,
639
+ toolCallId,
640
+ output,
641
+ options
642
+ }));
643
+ },
644
+ onRespondToToolApproval: (response_1) => {
645
+ if (supersededApprovalProjection.approvalIds.has(response_1.approvalId)) return Promise.reject(/* @__PURE__ */ new Error(`Tool approval ${response_1.approvalId} is not waiting for a response.`));
646
+ return customOnRespondToToolApproval ? respondViaHost(customOnRespondToToolApproval, response_1) : respondViaAISDK(response_1);
647
+ },
648
+ unstable_onRecordToolInteraction: ({ messageId: messageId_2, toolCallId: toolCallId_0, interaction }) => {
649
+ toolInteractionsRef.current.set(toolCallId_0, appendToolInteraction(toolInteractionsRef.current.get(toolCallId_0), interaction));
650
+ markToolInteractionsChanged();
651
+ return persistToolInteractions(messageId_2);
417
652
  },
418
- onRespondToToolApproval: customOnRespondToToolApproval ? (response_0) => respondViaHost(customOnRespondToToolApproval, response_0) : respondViaAISDK,
419
653
  ...pickExternalStoreSharedOptions(adapter),
420
654
  ...adapter.unstable_messageRepositoryInstance && { unstable_messageRepositoryInstance: adapter.unstable_messageRepositoryInstance },
421
655
  ...suggestionAdapter ? { suggestions: generatedSuggestions } : {},