@assistant-ui/ai-sdk 0.0.7 → 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 (105) 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 +1 -3
  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 +4 -6
  9. package/dist/aiSDKExtras.d.ts.map +1 -1
  10. package/dist/converters/convertMessage.d.ts +32 -24
  11. package/dist/converters/convertMessage.d.ts.map +1 -1
  12. package/dist/converters/convertMessage.js +43 -9
  13. package/dist/converters/convertMessage.js.map +1 -1
  14. package/dist/converters/modelContentEnvelope.d.ts +6 -7
  15. package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
  16. package/dist/converters/toCreateMessage.d.ts +2 -4
  17. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  18. package/dist/converters/toolOutputConversion.d.ts +20 -22
  19. package/dist/converters/toolOutputConversion.d.ts.map +1 -1
  20. package/dist/hooks.d.ts +2 -4
  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 +2 -4
  27. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  28. package/dist/model-context/injectQuoteContext.d.ts +1 -3
  29. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  30. package/dist/runtime/AISDKChat.d.ts +2 -4
  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 -31
  34. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  35. package/dist/runtime/AISDKThreads.js +22 -23
  36. package/dist/runtime/AISDKThreads.js.map +1 -1
  37. package/dist/runtime/sdkIdentity.d.ts +1 -3
  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 +69 -70
  41. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  42. package/dist/runtime/useAISDKRuntime.js +112 -56
  43. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  44. package/dist/runtime/useChatRuntime.d.ts +6 -8
  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 +94 -89
  49. package/dist/runtime/useChatThread.d.ts.map +1 -1
  50. package/dist/runtime/useChatThread.js +4 -5
  51. package/dist/runtime/useChatThread.js.map +1 -1
  52. package/dist/runtime/useExternalHistory.d.ts +7 -7
  53. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  54. package/dist/runtime/useExternalHistory.js +229 -59
  55. package/dist/runtime/useExternalHistory.js.map +1 -1
  56. package/dist/runtime/useResourceCleanup.d.ts +1 -3
  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 +3 -5
  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 +21 -23
  64. package/dist/tools/frontendTools.d.ts.map +1 -1
  65. package/dist/tools/generativeTools.d.ts +24 -26
  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 -4
  70. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
  71. package/dist/transport/AssistantChatTransport.d.ts +14 -16
  72. package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
  73. package/dist/transport/resumable.d.ts +25 -27
  74. package/dist/transport/resumable.d.ts.map +1 -1
  75. package/dist/usage.d.ts +8 -10
  76. package/dist/usage.d.ts.map +1 -1
  77. package/dist/usage.js.map +1 -1
  78. package/dist/utils/getVercelAIMessages.d.ts +2 -4
  79. package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
  80. package/dist/utils/sliceMessagesUntil.d.ts +1 -3
  81. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  82. package/package.json +9 -8
  83. package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
  84. package/src/adapters/vercelAttachmentAdapter.ts +2 -2
  85. package/src/converters/convertMessage.test.ts +478 -3
  86. package/src/converters/convertMessage.ts +100 -14
  87. package/src/index.native.ts +1 -1
  88. package/src/index.ts +1 -1
  89. package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
  90. package/src/runtime/AISDKThreads.test.ts +79 -46
  91. package/src/runtime/AISDKThreads.ts +1 -1
  92. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +51 -0
  93. package/src/runtime/useAISDKRuntime.approval.test.tsx +225 -9
  94. package/src/runtime/useAISDKRuntime.test.ts +275 -5
  95. package/src/runtime/useAISDKRuntime.ts +141 -37
  96. package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
  97. package/src/runtime/useChatRuntime.integration.test.tsx +56 -0
  98. package/src/runtime/useChatRuntime.test.ts +5 -1
  99. package/src/runtime/useChatRuntime.ts +6 -2
  100. package/src/runtime/useChatThread.transport.test.tsx +149 -0
  101. package/src/runtime/useChatThread.ts +33 -5
  102. package/src/runtime/useExternalHistory.test.ts +943 -3
  103. package/src/runtime/useExternalHistory.ts +366 -14
  104. package/src/runtime/useResourceCleanup.ts +41 -15
  105. 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,
@@ -55,6 +60,206 @@ const isTerminalMessage = (message: ThreadMessage) =>
55
60
  message.status.type === "complete" ||
56
61
  message.status.type === "incomplete";
57
62
 
63
+ const TOOL_ARTIFACTS_METADATA_KEY = "__aui_toolArtifacts";
64
+ const TOOL_INTERACTIONS_METADATA_KEY = "__aui_toolInteractions";
65
+ const TOOL_APPROVAL_RESPONSES_METADATA_KEY = "__aui_toolApprovalResponses";
66
+
67
+ type StoredToolApprovalResponse = Omit<
68
+ RespondToToolApprovalOptions,
69
+ "approvalId"
70
+ >;
71
+
72
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
73
+ value !== null && typeof value === "object" && !Array.isArray(value);
74
+
75
+ const collectToolArtifacts = (
76
+ message: ThreadMessage,
77
+ toolArtifacts: ReadonlyMap<string, unknown> | undefined,
78
+ ) => {
79
+ if (!toolArtifacts) return undefined;
80
+ const entries = message.content.flatMap((part) => {
81
+ if (part.type !== "tool-call") return [];
82
+ const artifact = toolArtifacts.get(part.toolCallId);
83
+ return artifact === undefined ? [] : [[part.toolCallId, artifact] as const];
84
+ });
85
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
86
+ };
87
+
88
+ const collectToolInteractions = (
89
+ message: ThreadMessage,
90
+ toolInteractions:
91
+ | ReadonlyMap<string, Unstable_ToolInteractionLog>
92
+ | undefined,
93
+ ) => {
94
+ if (!toolInteractions) return undefined;
95
+ const entries = message.content.flatMap((part) => {
96
+ if (part.type !== "tool-call") return [];
97
+ const interactions = toolInteractions.get(part.toolCallId);
98
+ return interactions === undefined
99
+ ? []
100
+ : [[part.toolCallId, interactions] as const];
101
+ });
102
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
103
+ };
104
+
105
+ const collectToolApprovalResponses = (
106
+ message: ThreadMessage,
107
+ toolApprovalResponses:
108
+ | ReadonlyMap<string, RespondToToolApprovalOptions>
109
+ | undefined,
110
+ ) => {
111
+ if (!toolApprovalResponses) return undefined;
112
+ const entries = message.content.flatMap((part) => {
113
+ if (part.type !== "tool-call" || !part.approval) return [];
114
+ const response = toolApprovalResponses.get(part.approval.id);
115
+ if (!response) return [];
116
+ return [
117
+ [
118
+ part.approval.id,
119
+ {
120
+ approved: response.approved,
121
+ ...(response.optionId != null && { optionId: response.optionId }),
122
+ ...(response.text != null && { text: response.text }),
123
+ ...(response.reason != null && { reason: response.reason }),
124
+ },
125
+ ] as const,
126
+ ];
127
+ });
128
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
129
+ };
130
+
131
+ const addToolData = <TMessage>(
132
+ message: TMessage,
133
+ toolArtifacts: Record<string, unknown> | undefined,
134
+ toolInteractions: Record<string, Unstable_ToolInteractionLog> | undefined,
135
+ toolApprovalResponses: Record<string, StoredToolApprovalResponse> | undefined,
136
+ ): TMessage => {
137
+ if (
138
+ (!toolArtifacts && !toolInteractions && !toolApprovalResponses) ||
139
+ !isRecord(message) ||
140
+ !Array.isArray(message.parts)
141
+ )
142
+ return message;
143
+ const toolCallIds = message.parts.flatMap((part) => {
144
+ if (!isRecord(part) || typeof part.toolCallId !== "string") return [];
145
+ return [part.toolCallId];
146
+ });
147
+ const artifacts = toolArtifacts
148
+ ? Object.fromEntries(
149
+ toolCallIds.flatMap((toolCallId) =>
150
+ Object.hasOwn(toolArtifacts, toolCallId)
151
+ ? [[toolCallId, toolArtifacts[toolCallId]] as const]
152
+ : [],
153
+ ),
154
+ )
155
+ : undefined;
156
+ const interactions = toolInteractions
157
+ ? Object.fromEntries(
158
+ toolCallIds.flatMap((toolCallId) =>
159
+ Object.hasOwn(toolInteractions, toolCallId)
160
+ ? [[toolCallId, toolInteractions[toolCallId]] as const]
161
+ : [],
162
+ ),
163
+ )
164
+ : undefined;
165
+ const approvalIds = message.parts.flatMap((part) => {
166
+ if (!isRecord(part) || !isRecord(part.approval)) return [];
167
+ const approvalId = part.approval.id;
168
+ return typeof approvalId === "string" ? [approvalId] : [];
169
+ });
170
+ const approvalResponses = toolApprovalResponses
171
+ ? Object.fromEntries(
172
+ approvalIds.flatMap((approvalId) =>
173
+ Object.hasOwn(toolApprovalResponses, approvalId)
174
+ ? [[approvalId, toolApprovalResponses[approvalId]] as const]
175
+ : [],
176
+ ),
177
+ )
178
+ : undefined;
179
+ const hasArtifacts = !!artifacts && Object.keys(artifacts).length > 0;
180
+ const hasInteractions =
181
+ !!interactions && Object.keys(interactions).length > 0;
182
+ const hasApprovalResponses =
183
+ !!approvalResponses && Object.keys(approvalResponses).length > 0;
184
+ if (!hasArtifacts && !hasInteractions && !hasApprovalResponses)
185
+ return message;
186
+ const metadata = isRecord(message.metadata) ? message.metadata : {};
187
+ return {
188
+ ...message,
189
+ metadata: {
190
+ ...metadata,
191
+ ...(hasArtifacts && { [TOOL_ARTIFACTS_METADATA_KEY]: artifacts }),
192
+ ...(hasInteractions && {
193
+ [TOOL_INTERACTIONS_METADATA_KEY]: interactions,
194
+ }),
195
+ ...(hasApprovalResponses && {
196
+ [TOOL_APPROVAL_RESPONSES_METADATA_KEY]: approvalResponses,
197
+ }),
198
+ },
199
+ } as TMessage;
200
+ };
201
+
202
+ const restoreToolData = <TMessage>(
203
+ message: TMessage,
204
+ toolArtifacts: Map<string, unknown> | undefined,
205
+ toolInteractions: Map<string, Unstable_ToolInteractionLog> | undefined,
206
+ toolApprovalResponses: Map<string, RespondToToolApprovalOptions> | undefined,
207
+ ): TMessage => {
208
+ if (!isRecord(message) || !isRecord(message.metadata)) return message;
209
+ const metadata = message.metadata;
210
+ const hasArtifacts = Object.hasOwn(metadata, TOOL_ARTIFACTS_METADATA_KEY);
211
+ const hasInteractions = Object.hasOwn(
212
+ metadata,
213
+ TOOL_INTERACTIONS_METADATA_KEY,
214
+ );
215
+ const hasApprovalResponses = Object.hasOwn(
216
+ metadata,
217
+ TOOL_APPROVAL_RESPONSES_METADATA_KEY,
218
+ );
219
+ if (!hasArtifacts && !hasInteractions && !hasApprovalResponses)
220
+ return message;
221
+ const artifacts = metadata[TOOL_ARTIFACTS_METADATA_KEY];
222
+ if (toolArtifacts && isRecord(artifacts)) {
223
+ for (const [toolCallId, artifact] of Object.entries(artifacts)) {
224
+ toolArtifacts.set(toolCallId, artifact);
225
+ }
226
+ }
227
+ const interactions = metadata[TOOL_INTERACTIONS_METADATA_KEY];
228
+ if (toolInteractions && isRecord(interactions)) {
229
+ for (const [toolCallId, value] of Object.entries(interactions)) {
230
+ const log = readToolInteractionLog(value);
231
+ if (log) toolInteractions.set(toolCallId, log);
232
+ }
233
+ }
234
+ const approvalResponses = metadata[TOOL_APPROVAL_RESPONSES_METADATA_KEY];
235
+ if (toolApprovalResponses && isRecord(approvalResponses)) {
236
+ for (const [approvalId, value] of Object.entries(approvalResponses)) {
237
+ if (!isRecord(value) || typeof value.approved !== "boolean") continue;
238
+ toolApprovalResponses.set(approvalId, {
239
+ approvalId,
240
+ approved: value.approved,
241
+ ...(typeof value.optionId === "string" && {
242
+ optionId: value.optionId,
243
+ }),
244
+ ...(typeof value.text === "string" && { text: value.text }),
245
+ ...(typeof value.reason === "string" && { reason: value.reason }),
246
+ });
247
+ }
248
+ }
249
+ const {
250
+ [TOOL_ARTIFACTS_METADATA_KEY]: _,
251
+ [TOOL_INTERACTIONS_METADATA_KEY]: __,
252
+ [TOOL_APPROVAL_RESPONSES_METADATA_KEY]: ___,
253
+ ...restMetadata
254
+ } = metadata;
255
+ const { metadata: _metadata, ...restMessage } = message;
256
+ return (
257
+ Object.keys(restMetadata).length === 0
258
+ ? restMessage
259
+ : { ...restMessage, metadata: restMetadata }
260
+ ) as TMessage;
261
+ };
262
+
58
263
  const encodeContent = <TMessage>(
59
264
  storageFormatAdapter: MessageFormatAdapter<TMessage, any>,
60
265
  item: MessageFormatItem<TMessage>,
@@ -66,6 +271,12 @@ export const useExternalHistory = <TMessage>(
66
271
  toThreadMessages: (messages: TMessage[]) => ThreadMessage[],
67
272
  storageFormatAdapter: MessageFormatAdapter<TMessage, any>,
68
273
  onSetMessages: (messages: TMessage[]) => void,
274
+ toolArtifacts?: Map<string, unknown>,
275
+ onToolArtifactsRestored?: () => void,
276
+ toolInteractions?: Map<string, Unstable_ToolInteractionLog>,
277
+ onToolInteractionsRestored?: () => void,
278
+ toolApprovalResponses?: Map<string, RespondToToolApprovalOptions>,
279
+ onToolApprovalResponsesRestored?: () => void,
69
280
  ) => {
70
281
  const loadedRef = useRef(false);
71
282
  const [itemEpoch, setItemEpoch] = useState(0);
@@ -108,17 +319,37 @@ export const useExternalHistory = <TMessage>(
108
319
  const loadHistory = async () => {
109
320
  try {
110
321
  const repo = await formatAdapter.load();
111
- if (repo && repo.messages.length > 0) {
112
- for (const m of repo.messages) {
322
+ toolArtifacts?.clear();
323
+ toolInteractions?.clear();
324
+ toolApprovalResponses?.clear();
325
+ const restoredMessages =
326
+ repo?.messages.map((item) => ({
327
+ ...item,
328
+ message: restoreToolData(
329
+ item.message,
330
+ toolArtifacts,
331
+ toolInteractions,
332
+ toolApprovalResponses,
333
+ ),
334
+ })) ?? [];
335
+ onToolArtifactsRestored?.();
336
+ onToolInteractionsRestored?.();
337
+ onToolApprovalResponsesRestored?.();
338
+ if (repo && restoredMessages.length > 0) {
339
+ const restoredRepo = { ...repo, messages: restoredMessages };
340
+ for (const [index, m] of repo.messages.entries()) {
113
341
  persistedInnerMessages.current.set(
114
342
  storageFormatAdapter.getId(m.message),
115
343
  {
116
- source: m.message,
344
+ source: restoredMessages[index]!.message,
117
345
  content: encodeContent(storageFormatAdapter, m),
118
346
  },
119
347
  );
120
348
  }
121
- const converted = toExportedMessageRepository(toThreadMessages, repo);
349
+ const converted = toExportedMessageRepository(
350
+ toThreadMessages,
351
+ restoredRepo,
352
+ );
122
353
  runtimeRef.current.thread.import(converted);
123
354
 
124
355
  const tempRepo = new MessageRepository();
@@ -177,6 +408,12 @@ export const useExternalHistory = <TMessage>(
177
408
  aui,
178
409
  itemEpoch,
179
410
  storageFormatAdapter,
411
+ toolArtifacts,
412
+ onToolArtifactsRestored,
413
+ toolInteractions,
414
+ onToolInteractionsRestored,
415
+ toolApprovalResponses,
416
+ onToolApprovalResponsesRestored,
180
417
  ]);
181
418
 
182
419
  const runStartRef = useRef<number | null>(null);
@@ -186,6 +423,84 @@ export const useExternalHistory = <TMessage>(
186
423
  const wasRunningRef = useRef(false);
187
424
  const toolCallCountRef = useRef(0);
188
425
 
426
+ const persistToolInteractions = useCallback(
427
+ (messageId: string) => {
428
+ const persistence = persistInFlightRef.current.then(async () => {
429
+ if (!formatAdapter?.update) return;
430
+ const messages = runtimeRef.current.thread.getState().messages;
431
+ const message = messages.find(
432
+ (item) =>
433
+ item.id === messageId ||
434
+ getExternalStoreMessages<TMessage>(item).some(
435
+ (innerMessage) =>
436
+ storageFormatAdapter.getId(innerMessage) === messageId,
437
+ ),
438
+ );
439
+ if (!message) return;
440
+
441
+ const previousMessages = messages
442
+ .slice(0, messages.indexOf(message))
443
+ .flatMap(getExternalStoreMessages<TMessage>);
444
+ let parentId = previousMessages.at(-1)
445
+ ? storageFormatAdapter.getId(previousMessages.at(-1)!)
446
+ : null;
447
+ const storedToolArtifacts = collectToolArtifacts(
448
+ message,
449
+ toolArtifacts,
450
+ );
451
+ const storedToolInteractions = collectToolInteractions(
452
+ message,
453
+ toolInteractions,
454
+ );
455
+ const storedToolApprovalResponses = collectToolApprovalResponses(
456
+ message,
457
+ toolApprovalResponses,
458
+ );
459
+
460
+ for (const innerMessage of getExternalStoreMessages<TMessage>(
461
+ message,
462
+ )) {
463
+ const item = {
464
+ parentId,
465
+ message: addToolData(
466
+ innerMessage,
467
+ storedToolArtifacts,
468
+ storedToolInteractions,
469
+ storedToolApprovalResponses,
470
+ ),
471
+ };
472
+ const innerId = storageFormatAdapter.getId(item.message);
473
+ const persisted = persistedInnerMessages.current.get(innerId);
474
+ if (persisted) {
475
+ const content = encodeContent(storageFormatAdapter, item);
476
+ if (content === persisted.content) {
477
+ persisted.source = item.message;
478
+ } else {
479
+ await formatAdapter.update(item, innerId);
480
+ persistedInnerMessages.current.set(innerId, {
481
+ source: item.message,
482
+ content,
483
+ });
484
+ }
485
+ }
486
+ parentId = innerId;
487
+ }
488
+ });
489
+ persistInFlightRef.current = persistence.catch(() => {});
490
+ return persistence.catch((error) => {
491
+ console.error("Failed to persist tool data:", error);
492
+ });
493
+ },
494
+ [
495
+ formatAdapter,
496
+ runtimeRef,
497
+ storageFormatAdapter,
498
+ toolArtifacts,
499
+ toolInteractions,
500
+ toolApprovalResponses,
501
+ ],
502
+ );
503
+
189
504
  useEffect(() => {
190
505
  if (!formatAdapter) return;
191
506
  const adapter = formatAdapter;
@@ -318,13 +633,27 @@ export const useExternalHistory = <TMessage>(
318
633
  const getLastInnerId = (msgs: TMessage[]): string | null =>
319
634
  msgs.length > 0 ? storageFormatAdapter.getId(msgs.at(-1)!) : null;
320
635
 
321
- const toBatchItems = (msgs: TMessage[]) =>
636
+ const toBatchItems = (
637
+ msgs: TMessage[],
638
+ toolArtifacts: Record<string, unknown> | undefined,
639
+ toolInteractions:
640
+ | Record<string, Unstable_ToolInteractionLog>
641
+ | undefined,
642
+ toolApprovalResponses:
643
+ | Record<string, StoredToolApprovalResponse>
644
+ | undefined,
645
+ ) =>
322
646
  msgs.map((msg, idx) => ({
323
647
  parentId:
324
648
  idx === 0
325
649
  ? lastInnerMessageId
326
650
  : storageFormatAdapter.getId(msgs[idx - 1]!),
327
- message: msg,
651
+ message: addToolData(
652
+ msg,
653
+ toolArtifacts,
654
+ toolInteractions,
655
+ toolApprovalResponses,
656
+ ),
328
657
  }));
329
658
 
330
659
  for (const message of messages) {
@@ -348,7 +677,12 @@ export const useExternalHistory = <TMessage>(
348
677
  deferredTelemetryIds.current.add(message.id);
349
678
  }
350
679
 
351
- const batchItems = toBatchItems(innerMessages);
680
+ const batchItems = toBatchItems(
681
+ innerMessages,
682
+ collectToolArtifacts(message, toolArtifacts),
683
+ collectToolInteractions(message, toolInteractions),
684
+ collectToolApprovalResponses(message, toolApprovalResponses),
685
+ );
352
686
  for (const item of batchItems) {
353
687
  const innerId = storageFormatAdapter.getId(item.message);
354
688
  const persisted = persistedInnerMessages.current.get(innerId);
@@ -385,10 +719,16 @@ export const useExternalHistory = <TMessage>(
385
719
 
386
720
  if (deferredTelemetryIds.current.has(message.id) && isTerminal) {
387
721
  deferredTelemetryIds.current.delete(message.id);
388
- adapter.reportTelemetry?.(batchItems, {
389
- ...telemetryOptions,
390
- message,
391
- });
722
+ adapter.reportTelemetry?.(
723
+ batchItems.map((item, index) => ({
724
+ ...item,
725
+ message: innerMessages[index]!,
726
+ })),
727
+ {
728
+ ...telemetryOptions,
729
+ message,
730
+ },
731
+ );
392
732
  }
393
733
  }
394
734
  })
@@ -405,7 +745,14 @@ export const useExternalHistory = <TMessage>(
405
745
  persistSettled(false);
406
746
  }
407
747
  };
408
- }, [formatAdapter, storageFormatAdapter, runtimeRef]);
748
+ }, [
749
+ formatAdapter,
750
+ storageFormatAdapter,
751
+ runtimeRef,
752
+ toolArtifacts,
753
+ toolInteractions,
754
+ toolApprovalResponses,
755
+ ]);
409
756
 
410
757
  const deleteMessage = useCallback(
411
758
  async (messageId: string) => {
@@ -448,5 +795,10 @@ export const useExternalHistory = <TMessage>(
448
795
  [formatAdapter, runtimeRef, storageFormatAdapter],
449
796
  );
450
797
 
451
- return { isLoading, deleteMessage };
798
+ return {
799
+ isLoading,
800
+ deleteMessage,
801
+ persistToolInteractions,
802
+ persistToolApprovalResponses: persistToolInteractions,
803
+ };
452
804
  };
@@ -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
  };
package/src/usage.ts CHANGED
@@ -1,4 +1,4 @@
1
- /// <reference types="@assistant-ui/core/react" />
1
+ /// <reference types="@assistant-ui/core/react" preserve="true" />
2
2
  import { useMemo } from "react";
3
3
  import { useAuiState } from "@assistant-ui/store";
4
4