@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
@@ -8,9 +8,14 @@ import type {
8
8
  MessageFormatItem,
9
9
  MessageFormatRepository,
10
10
  ExportedMessageRepository,
11
+ RespondToToolApprovalOptions,
12
+ Unstable_ToolInteractionLog,
11
13
  } from "@assistant-ui/core";
12
14
  import { getExternalStoreMessages } from "@assistant-ui/core";
13
- import { MessageRepository } from "@assistant-ui/core/internal";
15
+ import {
16
+ MessageRepository,
17
+ readToolInteractionLog,
18
+ } from "@assistant-ui/core/internal";
14
19
  import { useAui } from "@assistant-ui/store";
15
20
  import {
16
21
  useRef,
@@ -46,15 +51,226 @@ export const toExportedMessageRepository = <TMessage>(
46
51
  };
47
52
  };
48
53
 
54
+ const hasUnansweredApproval = (message: ThreadMessage) =>
55
+ message.content.some(
56
+ (part) =>
57
+ part.type === "tool-call" &&
58
+ part.approval != null &&
59
+ part.approval.approved === undefined &&
60
+ part.approval.resolution === undefined,
61
+ );
62
+
63
+ // Core reports a run paused on an unanswered approval as an interrupt, not as tool calls.
49
64
  const isAwaitingToolApproval = (message: ThreadMessage) =>
50
65
  message.status?.type === "requires-action" &&
51
- message.status.reason === "tool-calls";
66
+ (message.status.reason === "tool-calls" ||
67
+ (message.status.reason === "interrupt" && hasUnansweredApproval(message)));
52
68
 
53
69
  const isTerminalMessage = (message: ThreadMessage) =>
54
70
  message.status === undefined ||
55
71
  message.status.type === "complete" ||
56
72
  message.status.type === "incomplete";
57
73
 
74
+ const TOOL_ARTIFACTS_METADATA_KEY = "__aui_toolArtifacts";
75
+ const TOOL_INTERACTIONS_METADATA_KEY = "__aui_toolInteractions";
76
+ const TOOL_APPROVAL_RESPONSES_METADATA_KEY = "__aui_toolApprovalResponses";
77
+
78
+ type StoredToolApprovalResponse = Omit<
79
+ RespondToToolApprovalOptions,
80
+ "approvalId"
81
+ >;
82
+
83
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
84
+ value !== null && typeof value === "object" && !Array.isArray(value);
85
+
86
+ const collectToolArtifacts = (
87
+ message: ThreadMessage,
88
+ toolArtifacts: ReadonlyMap<string, unknown> | undefined,
89
+ ) => {
90
+ if (!toolArtifacts) return undefined;
91
+ const entries = message.content.flatMap((part) => {
92
+ if (part.type !== "tool-call") return [];
93
+ const artifact = toolArtifacts.get(part.toolCallId);
94
+ return artifact === undefined ? [] : [[part.toolCallId, artifact] as const];
95
+ });
96
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
97
+ };
98
+
99
+ const collectToolInteractions = (
100
+ message: ThreadMessage,
101
+ toolInteractions:
102
+ | ReadonlyMap<string, Unstable_ToolInteractionLog>
103
+ | undefined,
104
+ ) => {
105
+ if (!toolInteractions) return undefined;
106
+ const entries = message.content.flatMap((part) => {
107
+ if (part.type !== "tool-call") return [];
108
+ const interactions = toolInteractions.get(part.toolCallId);
109
+ return interactions === undefined
110
+ ? []
111
+ : [[part.toolCallId, interactions] as const];
112
+ });
113
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
114
+ };
115
+
116
+ const collectToolApprovalResponses = (
117
+ message: ThreadMessage,
118
+ toolApprovalResponses:
119
+ | ReadonlyMap<string, RespondToToolApprovalOptions>
120
+ | undefined,
121
+ ) => {
122
+ if (!toolApprovalResponses) return undefined;
123
+ const entries = message.content.flatMap((part) => {
124
+ if (part.type !== "tool-call" || !part.approval) return [];
125
+ const response = toolApprovalResponses.get(part.approval.id);
126
+ if (!response) return [];
127
+ return [
128
+ [
129
+ part.approval.id,
130
+ {
131
+ approved: response.approved,
132
+ ...(response.optionId != null && { optionId: response.optionId }),
133
+ ...(response.text != null && { text: response.text }),
134
+ ...(response.reason != null && { reason: response.reason }),
135
+ },
136
+ ] as const,
137
+ ];
138
+ });
139
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
140
+ };
141
+
142
+ const addToolData = <TMessage>(
143
+ message: TMessage,
144
+ toolArtifacts: Record<string, unknown> | undefined,
145
+ toolInteractions: Record<string, Unstable_ToolInteractionLog> | undefined,
146
+ toolApprovalResponses: Record<string, StoredToolApprovalResponse> | undefined,
147
+ ): TMessage => {
148
+ if (
149
+ (!toolArtifacts && !toolInteractions && !toolApprovalResponses) ||
150
+ !isRecord(message) ||
151
+ !Array.isArray(message.parts)
152
+ )
153
+ return message;
154
+ const toolCallIds = message.parts.flatMap((part) => {
155
+ if (!isRecord(part) || typeof part.toolCallId !== "string") return [];
156
+ return [part.toolCallId];
157
+ });
158
+ const artifacts = toolArtifacts
159
+ ? Object.fromEntries(
160
+ toolCallIds.flatMap((toolCallId) =>
161
+ Object.hasOwn(toolArtifacts, toolCallId)
162
+ ? [[toolCallId, toolArtifacts[toolCallId]] as const]
163
+ : [],
164
+ ),
165
+ )
166
+ : undefined;
167
+ const interactions = toolInteractions
168
+ ? Object.fromEntries(
169
+ toolCallIds.flatMap((toolCallId) =>
170
+ Object.hasOwn(toolInteractions, toolCallId)
171
+ ? [[toolCallId, toolInteractions[toolCallId]] as const]
172
+ : [],
173
+ ),
174
+ )
175
+ : undefined;
176
+ const approvalIds = message.parts.flatMap((part) => {
177
+ if (!isRecord(part) || !isRecord(part.approval)) return [];
178
+ const approvalId = part.approval.id;
179
+ return typeof approvalId === "string" ? [approvalId] : [];
180
+ });
181
+ const approvalResponses = toolApprovalResponses
182
+ ? Object.fromEntries(
183
+ approvalIds.flatMap((approvalId) =>
184
+ Object.hasOwn(toolApprovalResponses, approvalId)
185
+ ? [[approvalId, toolApprovalResponses[approvalId]] as const]
186
+ : [],
187
+ ),
188
+ )
189
+ : undefined;
190
+ const hasArtifacts = !!artifacts && Object.keys(artifacts).length > 0;
191
+ const hasInteractions =
192
+ !!interactions && Object.keys(interactions).length > 0;
193
+ const hasApprovalResponses =
194
+ !!approvalResponses && Object.keys(approvalResponses).length > 0;
195
+ if (!hasArtifacts && !hasInteractions && !hasApprovalResponses)
196
+ return message;
197
+ const metadata = isRecord(message.metadata) ? message.metadata : {};
198
+ return {
199
+ ...message,
200
+ metadata: {
201
+ ...metadata,
202
+ ...(hasArtifacts && { [TOOL_ARTIFACTS_METADATA_KEY]: artifacts }),
203
+ ...(hasInteractions && {
204
+ [TOOL_INTERACTIONS_METADATA_KEY]: interactions,
205
+ }),
206
+ ...(hasApprovalResponses && {
207
+ [TOOL_APPROVAL_RESPONSES_METADATA_KEY]: approvalResponses,
208
+ }),
209
+ },
210
+ } as TMessage;
211
+ };
212
+
213
+ const restoreToolData = <TMessage>(
214
+ message: TMessage,
215
+ toolArtifacts: Map<string, unknown> | undefined,
216
+ toolInteractions: Map<string, Unstable_ToolInteractionLog> | undefined,
217
+ toolApprovalResponses: Map<string, RespondToToolApprovalOptions> | undefined,
218
+ ): TMessage => {
219
+ if (!isRecord(message) || !isRecord(message.metadata)) return message;
220
+ const metadata = message.metadata;
221
+ const hasArtifacts = Object.hasOwn(metadata, TOOL_ARTIFACTS_METADATA_KEY);
222
+ const hasInteractions = Object.hasOwn(
223
+ metadata,
224
+ TOOL_INTERACTIONS_METADATA_KEY,
225
+ );
226
+ const hasApprovalResponses = Object.hasOwn(
227
+ metadata,
228
+ TOOL_APPROVAL_RESPONSES_METADATA_KEY,
229
+ );
230
+ if (!hasArtifacts && !hasInteractions && !hasApprovalResponses)
231
+ return message;
232
+ const artifacts = metadata[TOOL_ARTIFACTS_METADATA_KEY];
233
+ if (toolArtifacts && isRecord(artifacts)) {
234
+ for (const [toolCallId, artifact] of Object.entries(artifacts)) {
235
+ toolArtifacts.set(toolCallId, artifact);
236
+ }
237
+ }
238
+ const interactions = metadata[TOOL_INTERACTIONS_METADATA_KEY];
239
+ if (toolInteractions && isRecord(interactions)) {
240
+ for (const [toolCallId, value] of Object.entries(interactions)) {
241
+ const log = readToolInteractionLog(value);
242
+ if (log) toolInteractions.set(toolCallId, log);
243
+ }
244
+ }
245
+ const approvalResponses = metadata[TOOL_APPROVAL_RESPONSES_METADATA_KEY];
246
+ if (toolApprovalResponses && isRecord(approvalResponses)) {
247
+ for (const [approvalId, value] of Object.entries(approvalResponses)) {
248
+ if (!isRecord(value) || typeof value.approved !== "boolean") continue;
249
+ toolApprovalResponses.set(approvalId, {
250
+ approvalId,
251
+ approved: value.approved,
252
+ ...(typeof value.optionId === "string" && {
253
+ optionId: value.optionId,
254
+ }),
255
+ ...(typeof value.text === "string" && { text: value.text }),
256
+ ...(typeof value.reason === "string" && { reason: value.reason }),
257
+ });
258
+ }
259
+ }
260
+ const {
261
+ [TOOL_ARTIFACTS_METADATA_KEY]: _,
262
+ [TOOL_INTERACTIONS_METADATA_KEY]: __,
263
+ [TOOL_APPROVAL_RESPONSES_METADATA_KEY]: ___,
264
+ ...restMetadata
265
+ } = metadata;
266
+ const { metadata: _metadata, ...restMessage } = message;
267
+ return (
268
+ Object.keys(restMetadata).length === 0
269
+ ? restMessage
270
+ : { ...restMessage, metadata: restMetadata }
271
+ ) as TMessage;
272
+ };
273
+
58
274
  const encodeContent = <TMessage>(
59
275
  storageFormatAdapter: MessageFormatAdapter<TMessage, any>,
60
276
  item: MessageFormatItem<TMessage>,
@@ -66,6 +282,12 @@ export const useExternalHistory = <TMessage>(
66
282
  toThreadMessages: (messages: TMessage[]) => ThreadMessage[],
67
283
  storageFormatAdapter: MessageFormatAdapter<TMessage, any>,
68
284
  onSetMessages: (messages: TMessage[]) => void,
285
+ toolArtifacts?: Map<string, unknown>,
286
+ onToolArtifactsRestored?: () => void,
287
+ toolInteractions?: Map<string, Unstable_ToolInteractionLog>,
288
+ onToolInteractionsRestored?: () => void,
289
+ toolApprovalResponses?: Map<string, RespondToToolApprovalOptions>,
290
+ onToolApprovalResponsesRestored?: () => void,
69
291
  ) => {
70
292
  const loadedRef = useRef(false);
71
293
  const [itemEpoch, setItemEpoch] = useState(0);
@@ -108,17 +330,37 @@ export const useExternalHistory = <TMessage>(
108
330
  const loadHistory = async () => {
109
331
  try {
110
332
  const repo = await formatAdapter.load();
111
- if (repo && repo.messages.length > 0) {
112
- for (const m of repo.messages) {
333
+ toolArtifacts?.clear();
334
+ toolInteractions?.clear();
335
+ toolApprovalResponses?.clear();
336
+ const restoredMessages =
337
+ repo?.messages.map((item) => ({
338
+ ...item,
339
+ message: restoreToolData(
340
+ item.message,
341
+ toolArtifacts,
342
+ toolInteractions,
343
+ toolApprovalResponses,
344
+ ),
345
+ })) ?? [];
346
+ onToolArtifactsRestored?.();
347
+ onToolInteractionsRestored?.();
348
+ onToolApprovalResponsesRestored?.();
349
+ if (repo && restoredMessages.length > 0) {
350
+ const restoredRepo = { ...repo, messages: restoredMessages };
351
+ for (const [index, m] of repo.messages.entries()) {
113
352
  persistedInnerMessages.current.set(
114
353
  storageFormatAdapter.getId(m.message),
115
354
  {
116
- source: m.message,
355
+ source: restoredMessages[index]!.message,
117
356
  content: encodeContent(storageFormatAdapter, m),
118
357
  },
119
358
  );
120
359
  }
121
- const converted = toExportedMessageRepository(toThreadMessages, repo);
360
+ const converted = toExportedMessageRepository(
361
+ toThreadMessages,
362
+ restoredRepo,
363
+ );
122
364
  runtimeRef.current.thread.import(converted);
123
365
 
124
366
  const tempRepo = new MessageRepository();
@@ -177,6 +419,12 @@ export const useExternalHistory = <TMessage>(
177
419
  aui,
178
420
  itemEpoch,
179
421
  storageFormatAdapter,
422
+ toolArtifacts,
423
+ onToolArtifactsRestored,
424
+ toolInteractions,
425
+ onToolInteractionsRestored,
426
+ toolApprovalResponses,
427
+ onToolApprovalResponsesRestored,
180
428
  ]);
181
429
 
182
430
  const runStartRef = useRef<number | null>(null);
@@ -186,6 +434,84 @@ export const useExternalHistory = <TMessage>(
186
434
  const wasRunningRef = useRef(false);
187
435
  const toolCallCountRef = useRef(0);
188
436
 
437
+ const persistToolInteractions = useCallback(
438
+ (messageId: string) => {
439
+ const persistence = persistInFlightRef.current.then(async () => {
440
+ if (!formatAdapter?.update) return;
441
+ const messages = runtimeRef.current.thread.getState().messages;
442
+ const message = messages.find(
443
+ (item) =>
444
+ item.id === messageId ||
445
+ getExternalStoreMessages<TMessage>(item).some(
446
+ (innerMessage) =>
447
+ storageFormatAdapter.getId(innerMessage) === messageId,
448
+ ),
449
+ );
450
+ if (!message) return;
451
+
452
+ const previousMessages = messages
453
+ .slice(0, messages.indexOf(message))
454
+ .flatMap(getExternalStoreMessages<TMessage>);
455
+ let parentId = previousMessages.at(-1)
456
+ ? storageFormatAdapter.getId(previousMessages.at(-1)!)
457
+ : null;
458
+ const storedToolArtifacts = collectToolArtifacts(
459
+ message,
460
+ toolArtifacts,
461
+ );
462
+ const storedToolInteractions = collectToolInteractions(
463
+ message,
464
+ toolInteractions,
465
+ );
466
+ const storedToolApprovalResponses = collectToolApprovalResponses(
467
+ message,
468
+ toolApprovalResponses,
469
+ );
470
+
471
+ for (const innerMessage of getExternalStoreMessages<TMessage>(
472
+ message,
473
+ )) {
474
+ const item = {
475
+ parentId,
476
+ message: addToolData(
477
+ innerMessage,
478
+ storedToolArtifacts,
479
+ storedToolInteractions,
480
+ storedToolApprovalResponses,
481
+ ),
482
+ };
483
+ const innerId = storageFormatAdapter.getId(item.message);
484
+ const persisted = persistedInnerMessages.current.get(innerId);
485
+ if (persisted) {
486
+ const content = encodeContent(storageFormatAdapter, item);
487
+ if (content === persisted.content) {
488
+ persisted.source = item.message;
489
+ } else {
490
+ await formatAdapter.update(item, innerId);
491
+ persistedInnerMessages.current.set(innerId, {
492
+ source: item.message,
493
+ content,
494
+ });
495
+ }
496
+ }
497
+ parentId = innerId;
498
+ }
499
+ });
500
+ persistInFlightRef.current = persistence.catch(() => {});
501
+ return persistence.catch((error) => {
502
+ console.error("Failed to persist tool data:", error);
503
+ });
504
+ },
505
+ [
506
+ formatAdapter,
507
+ runtimeRef,
508
+ storageFormatAdapter,
509
+ toolArtifacts,
510
+ toolInteractions,
511
+ toolApprovalResponses,
512
+ ],
513
+ );
514
+
189
515
  useEffect(() => {
190
516
  if (!formatAdapter) return;
191
517
  const adapter = formatAdapter;
@@ -318,13 +644,27 @@ export const useExternalHistory = <TMessage>(
318
644
  const getLastInnerId = (msgs: TMessage[]): string | null =>
319
645
  msgs.length > 0 ? storageFormatAdapter.getId(msgs.at(-1)!) : null;
320
646
 
321
- const toBatchItems = (msgs: TMessage[]) =>
647
+ const toBatchItems = (
648
+ msgs: TMessage[],
649
+ toolArtifacts: Record<string, unknown> | undefined,
650
+ toolInteractions:
651
+ | Record<string, Unstable_ToolInteractionLog>
652
+ | undefined,
653
+ toolApprovalResponses:
654
+ | Record<string, StoredToolApprovalResponse>
655
+ | undefined,
656
+ ) =>
322
657
  msgs.map((msg, idx) => ({
323
658
  parentId:
324
659
  idx === 0
325
660
  ? lastInnerMessageId
326
661
  : storageFormatAdapter.getId(msgs[idx - 1]!),
327
- message: msg,
662
+ message: addToolData(
663
+ msg,
664
+ toolArtifacts,
665
+ toolInteractions,
666
+ toolApprovalResponses,
667
+ ),
328
668
  }));
329
669
 
330
670
  for (const message of messages) {
@@ -348,7 +688,12 @@ export const useExternalHistory = <TMessage>(
348
688
  deferredTelemetryIds.current.add(message.id);
349
689
  }
350
690
 
351
- const batchItems = toBatchItems(innerMessages);
691
+ const batchItems = toBatchItems(
692
+ innerMessages,
693
+ collectToolArtifacts(message, toolArtifacts),
694
+ collectToolInteractions(message, toolInteractions),
695
+ collectToolApprovalResponses(message, toolApprovalResponses),
696
+ );
352
697
  for (const item of batchItems) {
353
698
  const innerId = storageFormatAdapter.getId(item.message);
354
699
  const persisted = persistedInnerMessages.current.get(innerId);
@@ -385,10 +730,16 @@ export const useExternalHistory = <TMessage>(
385
730
 
386
731
  if (deferredTelemetryIds.current.has(message.id) && isTerminal) {
387
732
  deferredTelemetryIds.current.delete(message.id);
388
- adapter.reportTelemetry?.(batchItems, {
389
- ...telemetryOptions,
390
- message,
391
- });
733
+ adapter.reportTelemetry?.(
734
+ batchItems.map((item, index) => ({
735
+ ...item,
736
+ message: innerMessages[index]!,
737
+ })),
738
+ {
739
+ ...telemetryOptions,
740
+ message,
741
+ },
742
+ );
392
743
  }
393
744
  }
394
745
  })
@@ -405,7 +756,14 @@ export const useExternalHistory = <TMessage>(
405
756
  persistSettled(false);
406
757
  }
407
758
  };
408
- }, [formatAdapter, storageFormatAdapter, runtimeRef]);
759
+ }, [
760
+ formatAdapter,
761
+ storageFormatAdapter,
762
+ runtimeRef,
763
+ toolArtifacts,
764
+ toolInteractions,
765
+ toolApprovalResponses,
766
+ ]);
409
767
 
410
768
  const deleteMessage = useCallback(
411
769
  async (messageId: string) => {
@@ -448,5 +806,10 @@ export const useExternalHistory = <TMessage>(
448
806
  [formatAdapter, runtimeRef, storageFormatAdapter],
449
807
  );
450
808
 
451
- return { isLoading, deleteMessage };
809
+ return {
810
+ isLoading,
811
+ deleteMessage,
812
+ persistToolInteractions,
813
+ persistToolApprovalResponses: persistToolInteractions,
814
+ };
452
815
  };
@@ -1,11 +1,19 @@
1
1
  import { useEffect, useRef } from "react";
2
2
  import { useAssistantClientDestroySignal } from "@assistant-ui/store/internal";
3
3
 
4
- export const useResourceCleanup = (enabled: boolean, cleanup: () => void) => {
4
+ export const useResourceCleanup = (
5
+ enabled: boolean,
6
+ cleanup: () => void,
7
+ hostDestroySignal?: AbortSignal,
8
+ ) => {
5
9
  const destroySignal = useAssistantClientDestroySignal();
6
10
  const cleanupRef = useRef(cleanup);
7
11
  const enabledRef = useRef(enabled);
8
- const registeredSignalRef = useRef<AbortSignal | undefined>(undefined);
12
+ const registrationRef = useRef<{
13
+ destroySignal: AbortSignal | undefined;
14
+ hostDestroySignal: AbortSignal | undefined;
15
+ controller: AbortController;
16
+ } | null>(null);
9
17
 
10
18
  useEffect(() => {
11
19
  cleanupRef.current = cleanup;
@@ -13,20 +21,38 @@ export const useResourceCleanup = (enabled: boolean, cleanup: () => void) => {
13
21
  });
14
22
 
15
23
  useEffect(() => {
16
- if (!enabled || !destroySignal) return undefined;
17
- if (registeredSignalRef.current === destroySignal) return undefined;
24
+ if (!enabled || (!destroySignal && !hostDestroySignal)) return undefined;
25
+ const current = registrationRef.current;
26
+ if (
27
+ current !== null &&
28
+ current.destroySignal === destroySignal &&
29
+ current.hostDestroySignal === hostDestroySignal
30
+ ) {
31
+ return undefined;
32
+ }
33
+ current?.controller.abort();
18
34
 
19
- registeredSignalRef.current = destroySignal;
20
- destroySignal.addEventListener(
21
- "abort",
22
- () => {
23
- if (enabledRef.current) cleanupRef.current();
24
- },
25
- { once: true },
26
- );
35
+ const registration = new AbortController();
36
+ registrationRef.current = {
37
+ destroySignal,
38
+ hostDestroySignal,
39
+ controller: registration,
40
+ };
41
+ const run = () => {
42
+ if (registration.signal.aborted) return;
43
+ registration.abort();
44
+ if (enabledRef.current) cleanupRef.current();
45
+ };
46
+ if (destroySignal?.aborted || hostDestroySignal?.aborted) {
47
+ run();
48
+ return undefined;
49
+ }
50
+ const options = { once: true, signal: registration.signal };
51
+ destroySignal?.addEventListener("abort", run, options);
52
+ hostDestroySignal?.addEventListener("abort", run, options);
27
53
 
28
- // The listener must survive standalone soft unmounts so a later permanent
29
- // client destroy still cleans up the retained resource state.
54
+ // The listeners must survive standalone soft unmounts so a later
55
+ // permanent destroy still cleans up the retained resource state.
30
56
  return undefined;
31
- }, [destroySignal, enabled]);
57
+ }, [destroySignal, enabled, hostDestroySignal]);
32
58
  };