@assistant-ui/ai-sdk 0.0.11 → 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 (119) 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 +48 -22
  4. package/dist/converters/convertMessage.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.native.d.ts +1 -1
  9. package/dist/index.native.d.ts.map +1 -1
  10. package/dist/index.native.js +1 -1
  11. package/dist/model-context/injectInteractableContext.d.ts +3 -0
  12. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  13. package/dist/model-context/injectInteractableContext.js +3 -0
  14. package/dist/model-context/injectInteractableContext.js.map +1 -1
  15. package/dist/model-context/injectQuoteContext.d.ts +1 -0
  16. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  17. package/dist/model-context/injectQuoteContext.js +1 -0
  18. package/dist/model-context/injectQuoteContext.js.map +1 -1
  19. package/dist/runtime/AISDKChat.js +1 -1
  20. package/dist/runtime/AISDKThreads.d.ts +5 -0
  21. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  22. package/dist/runtime/AISDKThreads.js +37 -4
  23. package/dist/runtime/AISDKThreads.js.map +1 -1
  24. package/dist/runtime/DynamicChatTransport.d.ts +49 -0
  25. package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
  26. package/dist/runtime/DynamicChatTransport.js +147 -0
  27. package/dist/runtime/DynamicChatTransport.js.map +1 -0
  28. package/dist/runtime/getResumableAdapter.d.ts +5 -0
  29. package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
  30. package/dist/runtime/getResumableAdapter.js +12 -0
  31. package/dist/runtime/getResumableAdapter.js.map +1 -0
  32. package/dist/runtime/sdkIdentity.js +1 -1
  33. package/dist/runtime/toolHistoryCodec.d.ts +20 -0
  34. package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
  35. package/dist/runtime/toolHistoryCodec.js +107 -0
  36. package/dist/runtime/toolHistoryCodec.js.map +1 -0
  37. package/dist/runtime/useAISDKRuntime.d.ts +4 -1
  38. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  39. package/dist/runtime/useAISDKRuntime.js +154 -155
  40. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  41. package/dist/runtime/useChatRuntime.d.ts +14 -2
  42. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  43. package/dist/runtime/useChatRuntime.js +12 -3
  44. package/dist/runtime/useChatRuntime.js.map +1 -1
  45. package/dist/runtime/useChatThread.d.ts +3 -2
  46. package/dist/runtime/useChatThread.d.ts.map +1 -1
  47. package/dist/runtime/useChatThread.js +56 -36
  48. package/dist/runtime/useChatThread.js.map +1 -1
  49. package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
  50. package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
  51. package/dist/runtime/useDynamicChatTransport.js +64 -0
  52. package/dist/runtime/useDynamicChatTransport.js.map +1 -0
  53. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  54. package/dist/runtime/useExternalHistory.js +11 -109
  55. package/dist/runtime/useExternalHistory.js.map +1 -1
  56. package/dist/runtime/useResourceCleanup.js +1 -1
  57. package/dist/runtime/useStreamingTiming.js +2 -2
  58. package/dist/runtime/useStreamingTiming.js.map +1 -1
  59. package/dist/tools/generativeTools.d.ts +2 -1
  60. package/dist/tools/generativeTools.d.ts.map +1 -1
  61. package/dist/tools/generativeTools.js +5 -5
  62. package/dist/tools/generativeTools.js.map +1 -1
  63. package/dist/usage.js +1 -1
  64. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  65. package/dist/utils/sliceMessagesUntil.js +1 -2
  66. package/dist/utils/sliceMessagesUntil.js.map +1 -1
  67. package/package.json +11 -10
  68. package/src/converters/convertMessage.test.ts +120 -2
  69. package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
  70. package/src/converters/convertMessage.ts +60 -19
  71. package/src/converters/toCreateMessage.test.ts +27 -0
  72. package/src/index.native.ts +1 -1
  73. package/src/index.ts +1 -1
  74. package/src/model-context/injectInteractableContext.ts +3 -0
  75. package/src/model-context/injectQuoteContext.ts +1 -0
  76. package/src/runtime/AISDKChat.integration.test.tsx +57 -2
  77. package/src/runtime/AISDKThreads.cloud.test.ts +3 -0
  78. package/src/runtime/AISDKThreads.test.ts +181 -0
  79. package/src/runtime/AISDKThreads.ts +31 -4
  80. package/src/runtime/DynamicChatTransport.test.ts +203 -0
  81. package/src/runtime/DynamicChatTransport.ts +273 -0
  82. package/src/runtime/__tests__/controlled-transport.ts +3 -0
  83. package/src/runtime/getResumableAdapter.ts +16 -0
  84. package/src/runtime/toolHistoryCodec.test.ts +161 -0
  85. package/src/runtime/toolHistoryCodec.ts +207 -0
  86. package/src/runtime/useAISDKRuntime.approval.test.tsx +12 -0
  87. package/src/runtime/useAISDKRuntime.reload.test.tsx +219 -0
  88. package/src/runtime/useAISDKRuntime.test.ts +538 -5
  89. package/src/runtime/useAISDKRuntime.ts +161 -51
  90. package/src/runtime/useChatRuntime.integration.test.tsx +319 -4
  91. package/src/runtime/useChatRuntime.local-storage.test.tsx +123 -0
  92. package/src/runtime/useChatRuntime.test.ts +107 -1
  93. package/src/runtime/useChatRuntime.ts +26 -4
  94. package/src/runtime/useChatThread.binding.test.tsx +143 -0
  95. package/src/runtime/useChatThread.ts +98 -81
  96. package/src/runtime/useDynamicChatTransport.ts +26 -0
  97. package/src/runtime/useExternalHistory.test.ts +111 -0
  98. package/src/runtime/useExternalHistory.ts +14 -210
  99. package/src/runtime/useStreamingTiming.ts +2 -2
  100. package/src/tools/generativeTools.test.ts +1 -1
  101. package/src/tools/generativeTools.ts +5 -6
  102. package/src/utils/sliceMessagesUntil.test.ts +2 -6
  103. package/src/utils/sliceMessagesUntil.ts +1 -5
  104. package/dist/converters/modelContentEnvelope.d.ts +0 -14
  105. package/dist/converters/modelContentEnvelope.d.ts.map +0 -1
  106. package/dist/converters/modelContentEnvelope.js +0 -22
  107. package/dist/converters/modelContentEnvelope.js.map +0 -1
  108. package/dist/converters/toolOutputConversion.d.ts +0 -26
  109. package/dist/converters/toolOutputConversion.d.ts.map +0 -1
  110. package/dist/converters/toolOutputConversion.js +0 -31
  111. package/dist/converters/toolOutputConversion.js.map +0 -1
  112. package/dist/tools/frontendTools.d.ts +0 -30
  113. package/dist/tools/frontendTools.d.ts.map +0 -1
  114. package/dist/tools/frontendTools.js +0 -33
  115. package/dist/tools/frontendTools.js.map +0 -1
  116. package/src/converters/modelContentEnvelope.ts +0 -41
  117. package/src/converters/toolOutputConversion.ts +0 -26
  118. package/src/tools/frontendTools.test.ts +0 -205
  119. package/src/tools/frontendTools.ts +0 -83
@@ -7,6 +7,7 @@ import {
7
7
  useMemo,
8
8
  useRef,
9
9
  useState,
10
+ useSyncExternalStore,
10
11
  } from "react";
11
12
  import type {
12
13
  UIMessage,
@@ -60,7 +61,7 @@ import {
60
61
  AISDKMessageConverter,
61
62
  type AISDKMessageConverterMetadata,
62
63
  } from "../converters/convertMessage";
63
- import { wrapModelContentEnvelope } from "../converters/modelContentEnvelope";
64
+ import { wrapModelContentEnvelope } from "assistant-stream/internal";
64
65
  import {
65
66
  type AISDKStorageFormat,
66
67
  aiSDKV6FormatAdapter,
@@ -114,12 +115,15 @@ export type AISDKRuntimeAdapter<UI_MESSAGE extends UIMessage = UIMessage> =
114
115
  })
115
116
  | undefined;
116
117
  toCreateMessage?: CustomToCreateMessageFunction;
118
+ /** @deprecated Experimental since 2026-08-28. Not scheduled for removal; the API may change in any release. */
117
119
  unstable_messageRepositoryInstance?: MessageRepository | undefined;
118
120
  /**
119
121
  * The object a host answer belongs to, normally the `Chat` the runtime
120
122
  * renders. A host answer never reaches the `useChat` messages, so without
121
123
  * an owner it lives only as long as this runtime. With an owner, it
122
124
  * survives runtime remounts over the same chat.
125
+ *
126
+ * @deprecated Experimental since 2026-09-29. Not scheduled for removal; the API may change in any release.
123
127
  */
124
128
  unstable_hostApprovalOwner?: object | undefined;
125
129
  /**
@@ -183,7 +187,7 @@ export type AISDKRuntimeAdapter<UI_MESSAGE extends UIMessage = UIMessage> =
183
187
  * Called after an explicit `switchToBranch` (for example a BranchPicker
184
188
  * click). Complements `setMessages` and does not enable switching by itself.
185
189
  *
186
- * @deprecated This API is still under active development and might change without notice.
190
+ * @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
187
191
  */
188
192
  unstable_onBranchChange?: ExternalStoreAdapter["unstable_onBranchChange"];
189
193
  };
@@ -203,11 +207,25 @@ const useGeneratedSuggestions = (
203
207
  useInsertionEffect(() => {
204
208
  messagesRef.current = messages;
205
209
  }, [messages]);
210
+ const adapterKey = suggestionAdapter?.key;
206
211
  const adapterRef = useRef(suggestionAdapter);
212
+ const lastAdapterKeyRef = useRef(adapterKey);
213
+ const adapterReplacedRef = useRef(false);
207
214
  useInsertionEffect(() => {
215
+ const previousAdapter = adapterRef.current;
208
216
  adapterRef.current = suggestionAdapter;
209
- }, [suggestionAdapter]);
210
- const hasAdapter = suggestionAdapter != null;
217
+ const adapterReplaced =
218
+ previousAdapter !== undefined &&
219
+ suggestionAdapter !== undefined &&
220
+ lastAdapterKeyRef.current !== adapterKey;
221
+ adapterReplacedRef.current ||= adapterReplaced;
222
+ if (suggestionAdapter === undefined || adapterReplaced) {
223
+ controllerRef.current?.abort();
224
+ }
225
+ lastAdapterKeyRef.current = adapterKey;
226
+ }, [adapterKey, suggestionAdapter]);
227
+ const hasAdapter = suggestionAdapter !== undefined;
228
+ const adapterPresentRef = useRef(hasAdapter);
211
229
 
212
230
  useEffect(() => {
213
231
  const clearSuggestions = () => {
@@ -217,12 +235,18 @@ const useGeneratedSuggestions = (
217
235
  };
218
236
 
219
237
  const adapter = adapterRef.current;
238
+ const adapterRestored = hasAdapter && !adapterPresentRef.current;
239
+ adapterPresentRef.current = hasAdapter;
240
+ const adapterReplaced = adapterReplacedRef.current;
241
+ adapterReplacedRef.current = false;
220
242
  if (!adapter) {
221
243
  clearSuggestions();
222
244
  wasRunningRef.current = isRunning;
223
245
  return;
224
246
  }
225
247
 
248
+ if (adapterReplaced) clearSuggestions();
249
+
226
250
  if (isRunning) {
227
251
  if (!wasRunningRef.current) {
228
252
  clearSuggestions();
@@ -231,7 +255,7 @@ const useGeneratedSuggestions = (
231
255
  return;
232
256
  }
233
257
 
234
- if (!wasRunningRef.current) return;
258
+ if (!wasRunningRef.current && !adapterReplaced && !adapterRestored) return;
235
259
  wasRunningRef.current = false;
236
260
 
237
261
  const currentMessages = messagesRef.current;
@@ -252,11 +276,14 @@ const useGeneratedSuggestions = (
252
276
 
253
277
  await consumeSuggestionResult(promiseOrGenerator, {
254
278
  signal,
255
- onUpdate: setSuggestions,
279
+ onUpdate: (nextSuggestions) => {
280
+ if (controllerRef.current !== controller) return;
281
+ setSuggestions(nextSuggestions);
282
+ },
256
283
  });
257
284
  } catch {}
258
285
  })();
259
- }, [hasAdapter, isRunning]);
286
+ }, [adapterKey, hasAdapter, isRunning]);
260
287
 
261
288
  useReplaySafeEffect(() => {
262
289
  return () => {
@@ -290,10 +317,25 @@ type OwnedApproval = {
290
317
  response: RespondToToolApprovalOptions;
291
318
  };
292
319
 
293
- const hostToolApprovalsByChat = new WeakMap<
294
- object,
295
- Map<string, OwnedApproval>
296
- >();
320
+ type ChatRuntimeState = {
321
+ approvals: Map<string, OwnedApproval>;
322
+ cancelledMessageIds: ReadonlySet<string>;
323
+ };
324
+
325
+ const chatRuntimeStates = new WeakMap<object, ChatRuntimeState>();
326
+
327
+ const getChatRuntimeState = (owner: object | undefined) => {
328
+ if (owner === undefined) return undefined;
329
+ let state = chatRuntimeStates.get(owner);
330
+ if (state === undefined) {
331
+ state = {
332
+ approvals: new Map(),
333
+ cancelledMessageIds: NO_CANCELLED_MESSAGE_IDS,
334
+ };
335
+ chatRuntimeStates.set(owner, state);
336
+ }
337
+ return state;
338
+ };
297
339
 
298
340
  const toApprovalResponses = (
299
341
  owned: ReadonlyMap<string, OwnedApproval> | undefined,
@@ -308,19 +350,19 @@ const toApprovalResponses = (
308
350
  * may already be unmounted (a rollback resolving after a remount), and another
309
351
  * runtime may be mounted over the same owner.
310
352
  */
311
- const hostApprovalListenersByChat = new WeakMap<object, Set<() => void>>();
353
+ const chatRuntimeListeners = new WeakMap<object, Set<() => void>>();
312
354
 
313
- const subscribeToHostApprovals = (owner: object, listener: () => void) => {
314
- const listeners = hostApprovalListenersByChat.get(owner) ?? new Set();
315
- hostApprovalListenersByChat.set(owner, listeners);
355
+ const subscribeToChatRuntimeState = (owner: object, listener: () => void) => {
356
+ const listeners = chatRuntimeListeners.get(owner) ?? new Set();
357
+ chatRuntimeListeners.set(owner, listeners);
316
358
  listeners.add(listener);
317
359
  return () => {
318
360
  listeners.delete(listener);
319
361
  };
320
362
  };
321
363
 
322
- const notifyHostApprovals = (owner: object) => {
323
- for (const listener of [...(hostApprovalListenersByChat.get(owner) ?? [])]) {
364
+ const notifyChatRuntimeState = (owner: object) => {
365
+ for (const listener of [...(chatRuntimeListeners.get(owner) ?? [])]) {
324
366
  listener();
325
367
  }
326
368
  };
@@ -343,6 +385,7 @@ const getSupersededApprovalProjection = <UI_MESSAGE extends UIMessage>(
343
385
  lastAssistant === messages[lastIndex] &&
344
386
  lastAssistant?.parts?.some((part) => {
345
387
  if (
388
+ typeof part?.type !== "string" ||
346
389
  !isToolUIPart(part) ||
347
390
  part.state === "output-available" ||
348
391
  part.state === "output-error" ||
@@ -391,7 +434,12 @@ const getSupersededApprovalProjection = <UI_MESSAGE extends UIMessage>(
391
434
  }
392
435
 
393
436
  for (const part of message.parts ?? []) {
394
- if (!isToolUIPart(part) || part.state !== "approval-requested") continue;
437
+ if (
438
+ typeof part?.type !== "string" ||
439
+ !isToolUIPart(part) ||
440
+ part.state !== "approval-requested"
441
+ )
442
+ continue;
395
443
 
396
444
  const approval = part.approval;
397
445
  if (!approval) continue;
@@ -427,7 +475,10 @@ const findRawToolMessageIndex = <UI_MESSAGE extends UIMessage>(
427
475
  ) => {
428
476
  const containsToolCall = (message: UI_MESSAGE) =>
429
477
  message.parts?.some(
430
- (part) => isToolUIPart(part) && part.toolCallId === toolCallId,
478
+ (part) =>
479
+ typeof part?.type === "string" &&
480
+ isToolUIPart(part) &&
481
+ part.toolCallId === toolCallId,
431
482
  ) === true;
432
483
 
433
484
  const messageIndex = messages.findIndex(
@@ -491,14 +542,22 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
491
542
  // Set by hosts that own the chat across runtime lifetimes, so a host answer
492
543
  // outlives a remount over that same chat.
493
544
  const approvalOwner = adapter.unstable_hostApprovalOwner;
494
- const ownedApprovals = approvalOwner
495
- ? (hostToolApprovalsByChat.get(approvalOwner) ??
496
- (() => {
497
- const created = new Map<string, OwnedApproval>();
498
- hostToolApprovalsByChat.set(approvalOwner, created);
499
- return created;
500
- })())
501
- : undefined;
545
+ const chatRuntimeState = getChatRuntimeState(approvalOwner);
546
+ const ownedApprovals = chatRuntimeState?.approvals;
547
+ const ownedCancelledMessageIds = useSyncExternalStore(
548
+ useCallback(
549
+ (listener) =>
550
+ approvalOwner
551
+ ? subscribeToChatRuntimeState(approvalOwner, listener)
552
+ : () => {},
553
+ [approvalOwner],
554
+ ),
555
+ useCallback(
556
+ () => chatRuntimeState?.cancelledMessageIds ?? NO_CANCELLED_MESSAGE_IDS,
557
+ [chatRuntimeState],
558
+ ),
559
+ () => NO_CANCELLED_MESSAGE_IDS,
560
+ );
502
561
  const [toolApprovalResponses, setToolApprovalResponses] = useState<
503
562
  ReadonlyMap<string, RespondToToolApprovalOptions>
504
563
  >(() => toApprovalResponses(ownedApprovals));
@@ -532,7 +591,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
532
591
  return unchanged ? prev : next;
533
592
  });
534
593
  };
535
- const unsubscribe = subscribeToHostApprovals(approvalOwner, sync);
594
+ const unsubscribe = subscribeToChatRuntimeState(approvalOwner, sync);
536
595
  // The state was seeded during render, so a write landing between then and
537
596
  // this subscription would otherwise never be seen.
538
597
  sync();
@@ -559,7 +618,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
559
618
  new Map(),
560
619
  );
561
620
  const toolArgsTextCacheRef = useRef<
562
- WeakMap<ReadonlyJSONObject, Map<string, string>>
621
+ NonNullable<AISDKMessageConverterMetadata["toolArgsTextCache"]>
563
622
  >(new WeakMap());
564
623
  const mcpAppMetadataCacheRef = useRef<Map<string, McpAppMetadata>>(new Map());
565
624
  const toolArtifactsRef = useRef<Map<string, unknown>>(new Map());
@@ -590,8 +649,9 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
590
649
  const optimisticMessageId =
591
650
  isRunning && lastMessage?.role === "assistant" ? lastMessage.id : undefined;
592
651
 
593
- const cancelledMessageIds =
594
- cancelledMessages?.chatId === chatHelpers.id
652
+ const cancelledMessageIds = chatRuntimeState
653
+ ? ownedCancelledMessageIds
654
+ : cancelledMessages?.chatId === chatHelpers.id
595
655
  ? cancelledMessages.ids
596
656
  : NO_CANCELLED_MESSAGE_IDS;
597
657
  const supportsRichToolApprovalResponses =
@@ -646,6 +706,14 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
646
706
 
647
707
  const retractCancellation = useCallback(
648
708
  (chatId: string, messageId: string) => {
709
+ if (chatRuntimeState !== undefined && approvalOwner !== undefined) {
710
+ if (!chatRuntimeState.cancelledMessageIds.has(messageId)) return;
711
+ const ids = new Set(chatRuntimeState.cancelledMessageIds);
712
+ ids.delete(messageId);
713
+ chatRuntimeState.cancelledMessageIds = ids;
714
+ notifyChatRuntimeState(approvalOwner);
715
+ return;
716
+ }
649
717
  setCancelledMessages((prev) => {
650
718
  if (prev?.chatId !== chatId || !prev.ids.has(messageId)) return prev;
651
719
  const ids = new Set(prev.ids);
@@ -653,7 +721,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
653
721
  return { chatId, ids };
654
722
  });
655
723
  },
656
- [],
724
+ [approvalOwner, chatRuntimeState],
657
725
  );
658
726
 
659
727
  // A provider run that resumes the stopped response retracts its cancellation;
@@ -801,7 +869,8 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
801
869
 
802
870
  let messageChanged = false;
803
871
  const parts = message.parts?.map((part) => {
804
- if (!isToolUIPart(part)) return part;
872
+ if (typeof part?.type !== "string" || !isToolUIPart(part))
873
+ return part;
805
874
  if (
806
875
  part.state === "output-available" ||
807
876
  part.state === "output-error" ||
@@ -850,7 +919,9 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
850
919
  const requested = chatHelpers.messages
851
920
  .flatMap((message) =>
852
921
  message.parts.flatMap((part) =>
853
- isToolUIPart(part) ? [{ messageId: message.id, part }] : [],
922
+ typeof part?.type === "string" && isToolUIPart(part)
923
+ ? [{ messageId: message.id, part }]
924
+ : [],
854
925
  ),
855
926
  )
856
927
  .find(
@@ -888,7 +959,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
888
959
  if (startedOwner) {
889
960
  // Every runtime mounted over that owner re-reads the record, including
890
961
  // one mounted after this response started.
891
- notifyHostApprovals(startedOwner);
962
+ notifyChatRuntimeState(startedOwner);
892
963
  return;
893
964
  }
894
965
 
@@ -924,6 +995,20 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
924
995
  }
925
996
  };
926
997
 
998
+ // A thread message can join several AI SDK messages, so a slice through it
999
+ // has to end at the last of them rather than at the id the thread exposes.
1000
+ const lastAISDKMessageId = (threadMessageId: string | null) => {
1001
+ if (threadMessageId == null) return null;
1002
+ const threadMessage = runtimeRef.current.thread
1003
+ .getState()
1004
+ .messages.find((message) => message.id === threadMessageId);
1005
+ return (
1006
+ (threadMessage &&
1007
+ getVercelAIMessages<UI_MESSAGE>(threadMessage).at(-1)?.id) ??
1008
+ threadMessageId
1009
+ );
1010
+ };
1011
+
927
1012
  const hasSeededRepositoryRef = useRef(false);
928
1013
  const shouldFeedRepository =
929
1014
  exportedMessageRepository != null &&
@@ -1008,16 +1093,26 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
1008
1093
  isRunning && message?.role === "assistant" ? message.id : undefined;
1009
1094
  if (cancelledId) {
1010
1095
  const liveIds = new Set(chatHelpers.messages.map((m) => m.id));
1011
- setCancelledMessages((prev) => {
1012
- const kept =
1013
- prev?.chatId === chatHelpers.id
1014
- ? [...prev.ids].filter((id) => liveIds.has(id))
1015
- : [];
1016
- return {
1017
- chatId: chatHelpers.id,
1018
- ids: new Set([...kept, cancelledId]),
1019
- };
1020
- });
1096
+ if (chatRuntimeState !== undefined && approvalOwner !== undefined) {
1097
+ chatRuntimeState.cancelledMessageIds = new Set([
1098
+ ...[...chatRuntimeState.cancelledMessageIds].filter((id) =>
1099
+ liveIds.has(id),
1100
+ ),
1101
+ cancelledId,
1102
+ ]);
1103
+ notifyChatRuntimeState(approvalOwner);
1104
+ } else {
1105
+ setCancelledMessages((prev) => {
1106
+ const kept =
1107
+ prev?.chatId === chatHelpers.id
1108
+ ? [...prev.ids].filter((id) => liveIds.has(id))
1109
+ : [];
1110
+ return {
1111
+ chatId: chatHelpers.id,
1112
+ ids: new Set([...kept, cancelledId]),
1113
+ };
1114
+ });
1115
+ }
1021
1116
  }
1022
1117
  try {
1023
1118
  await chatHelpers.stop();
@@ -1052,9 +1147,10 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
1052
1147
  customToCreateMessage ?? toCreateMessage
1053
1148
  )<UI_MESSAGE>(message);
1054
1149
 
1150
+ const parentId = lastAISDKMessageId(message.parentId);
1055
1151
  if (!(message.startRun ?? message.role === "user")) {
1056
1152
  chatHelpers.setMessages((current) => [
1057
- ...sliceMessagesUntil(current, message.parentId),
1153
+ ...sliceMessagesUntil(current, parentId),
1058
1154
  toUIMessage<UI_MESSAGE>(createMessage, message.role),
1059
1155
  ]);
1060
1156
  return;
@@ -1062,7 +1158,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
1062
1158
 
1063
1159
  lastRunConfigRef.current = message.runConfig;
1064
1160
  chatHelpers.setMessages((current) =>
1065
- sliceMessagesUntil(current, message.parentId),
1161
+ sliceMessagesUntil(current, parentId),
1066
1162
  );
1067
1163
  await chatHelpers.sendMessage(createMessage, {
1068
1164
  metadata: message.runConfig,
@@ -1103,7 +1199,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
1103
1199
  if (removedToolArtifact) markToolArtifactsChanged();
1104
1200
  if (removedToolInteractions) markToolInteractionsChanged();
1105
1201
  if (removedToolApprovalResponse || removedHostApprovalId) {
1106
- if (approvalOwner) notifyHostApprovals(approvalOwner);
1202
+ if (approvalOwner) notifyChatRuntimeState(approvalOwner);
1107
1203
  setToolApprovalResponses(new Map(toolApprovalResponsesRef.current));
1108
1204
  }
1109
1205
 
@@ -1118,7 +1214,14 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
1118
1214
  },
1119
1215
  onReload: async (parentId: string | null, config) => {
1120
1216
  lastRunConfigRef.current = config.runConfig;
1121
- const newMessages = sliceMessagesUntil(chatHelpers.messages, parentId);
1217
+ const newMessages = sliceMessagesUntil(
1218
+ chatHelpers.messages,
1219
+ lastAISDKMessageId(parentId),
1220
+ );
1221
+ // regenerate drops a trailing assistant message, so the first message of
1222
+ // the reloaded response stays for it to drop instead of the parent.
1223
+ const reloaded = chatHelpers.messages[newMessages.length];
1224
+ if (reloaded?.role === "assistant") newMessages.push(reloaded);
1122
1225
  chatHelpers.setMessages(newMessages);
1123
1226
 
1124
1227
  await chatHelpers.regenerate({ metadata: config.runConfig });
@@ -1154,7 +1257,10 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
1154
1257
  if (targetIndex >= 0 && targetIndex !== chatHelpers.messages.length - 1) {
1155
1258
  const target = chatHelpers.messages[targetIndex]!;
1156
1259
  const targetPart = target.parts.find(
1157
- (part) => isToolUIPart(part) && part.toolCallId === toolCallId,
1260
+ (part) =>
1261
+ typeof part?.type === "string" &&
1262
+ isToolUIPart(part) &&
1263
+ part.toolCallId === toolCallId,
1158
1264
  ) as { state?: string; preliminary?: boolean } | undefined;
1159
1265
  // An earlier message's settled output may already have reached the model, as the error a cancelling send writes does.
1160
1266
  if (
@@ -1172,7 +1278,11 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
1172
1278
  return {
1173
1279
  ...message,
1174
1280
  parts: message.parts.map((part) => {
1175
- if (!isToolUIPart(part) || part.toolCallId !== toolCallId)
1281
+ if (
1282
+ typeof part?.type !== "string" ||
1283
+ !isToolUIPart(part) ||
1284
+ part.toolCallId !== toolCallId
1285
+ )
1176
1286
  return part;
1177
1287
 
1178
1288
  const { preliminary: _preliminary, ...finalPart } =