@ulearning/ai-sdk-adapter 0.0.0-stage → 1.0.0-next.0

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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/dist/adapters/aiSDKFormatAdapter.d.ts +6 -0
  4. package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
  5. package/dist/adapters/aiSDKFormatAdapter.js +17 -0
  6. package/dist/adapters/aiSDKFormatAdapter.js.map +1 -0
  7. package/dist/adapters/vercelAttachmentAdapter.d.ts +3 -0
  8. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -0
  9. package/dist/adapters/vercelAttachmentAdapter.js +36 -0
  10. package/dist/adapters/vercelAttachmentAdapter.js.map +1 -0
  11. package/dist/aiSDKExtras.d.ts +13 -0
  12. package/dist/aiSDKExtras.d.ts.map +1 -0
  13. package/dist/aiSDKExtras.js +6 -0
  14. package/dist/aiSDKExtras.js.map +1 -0
  15. package/dist/converters/convertMessage.d.ts +44 -0
  16. package/dist/converters/convertMessage.d.ts.map +1 -0
  17. package/dist/converters/convertMessage.js +465 -0
  18. package/dist/converters/convertMessage.js.map +1 -0
  19. package/dist/converters/toCreateMessage.d.ts +4 -0
  20. package/dist/converters/toCreateMessage.d.ts.map +1 -0
  21. package/dist/converters/toCreateMessage.js +65 -0
  22. package/dist/converters/toCreateMessage.js.map +1 -0
  23. package/dist/converters/toolApprovalAnswers.d.ts +3 -0
  24. package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
  25. package/dist/converters/toolApprovalAnswers.js +16 -0
  26. package/dist/converters/toolApprovalAnswers.js.map +1 -0
  27. package/dist/hooks.d.ts +17 -0
  28. package/dist/hooks.d.ts.map +1 -0
  29. package/dist/hooks.js +19 -0
  30. package/dist/hooks.js.map +1 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +13 -0
  34. package/dist/model-context/injectInteractableContext.d.ts +37 -0
  35. package/dist/model-context/injectInteractableContext.d.ts.map +1 -0
  36. package/dist/model-context/injectInteractableContext.js +54 -0
  37. package/dist/model-context/injectInteractableContext.js.map +1 -0
  38. package/dist/model-context/injectQuoteContext.d.ts +25 -0
  39. package/dist/model-context/injectQuoteContext.d.ts.map +1 -0
  40. package/dist/model-context/injectQuoteContext.js +51 -0
  41. package/dist/model-context/injectQuoteContext.js.map +1 -0
  42. package/dist/runtime/AISDKChat.d.ts +13 -0
  43. package/dist/runtime/AISDKChat.d.ts.map +1 -0
  44. package/dist/runtime/AISDKChat.js +34 -0
  45. package/dist/runtime/AISDKChat.js.map +1 -0
  46. package/dist/runtime/AISDKThreads.d.ts +24 -0
  47. package/dist/runtime/AISDKThreads.d.ts.map +1 -0
  48. package/dist/runtime/AISDKThreads.js +92 -0
  49. package/dist/runtime/AISDKThreads.js.map +1 -0
  50. package/dist/runtime/DynamicChatTransport.d.ts +50 -0
  51. package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
  52. package/dist/runtime/DynamicChatTransport.js +153 -0
  53. package/dist/runtime/DynamicChatTransport.js.map +1 -0
  54. package/dist/runtime/getResumableAdapter.d.ts +5 -0
  55. package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
  56. package/dist/runtime/getResumableAdapter.js +11 -0
  57. package/dist/runtime/getResumableAdapter.js.map +1 -0
  58. package/dist/runtime/toolHistoryCodec.d.ts +20 -0
  59. package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
  60. package/dist/runtime/toolHistoryCodec.js +122 -0
  61. package/dist/runtime/toolHistoryCodec.js.map +1 -0
  62. package/dist/runtime/useAISDKRuntime.d.ts +95 -0
  63. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -0
  64. package/dist/runtime/useAISDKRuntime.js +754 -0
  65. package/dist/runtime/useAISDKRuntime.js.map +1 -0
  66. package/dist/runtime/useChatRuntime.d.ts +17 -0
  67. package/dist/runtime/useChatRuntime.d.ts.map +1 -0
  68. package/dist/runtime/useChatRuntime.js +50 -0
  69. package/dist/runtime/useChatRuntime.js.map +1 -0
  70. package/dist/runtime/useChatSnapshot.d.ts +6 -0
  71. package/dist/runtime/useChatSnapshot.d.ts.map +1 -0
  72. package/dist/runtime/useChatSnapshot.js +62 -0
  73. package/dist/runtime/useChatSnapshot.js.map +1 -0
  74. package/dist/runtime/useChatThread.d.ts +121 -0
  75. package/dist/runtime/useChatThread.d.ts.map +1 -0
  76. package/dist/runtime/useChatThread.js +255 -0
  77. package/dist/runtime/useChatThread.js.map +1 -0
  78. package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
  79. package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
  80. package/dist/runtime/useDynamicChatTransport.js +26 -0
  81. package/dist/runtime/useDynamicChatTransport.js.map +1 -0
  82. package/dist/runtime/useExternalHistory.d.ts +12 -0
  83. package/dist/runtime/useExternalHistory.d.ts.map +1 -0
  84. package/dist/runtime/useExternalHistory.js +556 -0
  85. package/dist/runtime/useExternalHistory.js.map +1 -0
  86. package/dist/runtime/useMessageQueue.d.ts +14 -0
  87. package/dist/runtime/useMessageQueue.d.ts.map +1 -0
  88. package/dist/runtime/useMessageQueue.js +142 -0
  89. package/dist/runtime/useMessageQueue.js.map +1 -0
  90. package/dist/runtime/useResourceCleanup.d.ts +2 -0
  91. package/dist/runtime/useResourceCleanup.d.ts.map +1 -0
  92. package/dist/runtime/useResourceCleanup.js +47 -0
  93. package/dist/runtime/useResourceCleanup.js.map +1 -0
  94. package/dist/runtime/useStreamingTiming.d.ts +12 -0
  95. package/dist/runtime/useStreamingTiming.d.ts.map +1 -0
  96. package/dist/runtime/useStreamingTiming.js +36 -0
  97. package/dist/runtime/useStreamingTiming.js.map +1 -0
  98. package/dist/tools/generativeTools.d.ts +71 -0
  99. package/dist/tools/generativeTools.d.ts.map +1 -0
  100. package/dist/tools/generativeTools.js +258 -0
  101. package/dist/tools/generativeTools.js.map +1 -0
  102. package/dist/tools/mcp-stdio.node.d.ts +2 -0
  103. package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
  104. package/dist/tools/mcp-stdio.node.js +1 -0
  105. package/dist/tools/mcp-stdio.unsupported.d.ts +3 -0
  106. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -0
  107. package/dist/tools/mcp-stdio.unsupported.js +10 -0
  108. package/dist/tools/mcp-stdio.unsupported.js.map +1 -0
  109. package/dist/transport/AssistantChatTransport.d.ts +20 -0
  110. package/dist/transport/AssistantChatTransport.d.ts.map +1 -0
  111. package/dist/transport/AssistantChatTransport.js +151 -0
  112. package/dist/transport/AssistantChatTransport.js.map +1 -0
  113. package/dist/transport/resumable.d.ts +36 -0
  114. package/dist/transport/resumable.d.ts.map +1 -0
  115. package/dist/transport/resumable.js +112 -0
  116. package/dist/transport/resumable.js.map +1 -0
  117. package/dist/usage.d.ts +24 -0
  118. package/dist/usage.d.ts.map +1 -0
  119. package/dist/usage.js +121 -0
  120. package/dist/usage.js.map +1 -0
  121. package/dist/utils/getVercelAIMessages.d.ts +4 -0
  122. package/dist/utils/getVercelAIMessages.d.ts.map +1 -0
  123. package/dist/utils/getVercelAIMessages.js +8 -0
  124. package/dist/utils/getVercelAIMessages.js.map +1 -0
  125. package/dist/utils/sliceMessagesUntil.d.ts +3 -0
  126. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -0
  127. package/dist/utils/sliceMessagesUntil.js +10 -0
  128. package/dist/utils/sliceMessagesUntil.js.map +1 -0
  129. package/package.json +96 -3
  130. package/src/__tests__/standalone.e2e.ts +118 -0
  131. package/src/adapters/aiSDKFormatAdapter.test.ts +36 -0
  132. package/src/adapters/aiSDKFormatAdapter.ts +18 -0
  133. package/src/adapters/vercelAttachmentAdapter.test.ts +52 -0
  134. package/src/adapters/vercelAttachmentAdapter.ts +36 -0
  135. package/src/aiSDKExtras.ts +31 -0
  136. package/src/converters/convertMessage.metadata-key.test.tsx +182 -0
  137. package/src/converters/convertMessage.test.ts +2428 -0
  138. package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
  139. package/src/converters/convertMessage.ts +919 -0
  140. package/src/converters/toCreateMessage.test.ts +716 -0
  141. package/src/converters/toCreateMessage.ts +110 -0
  142. package/src/converters/toolApprovalAnswers.ts +27 -0
  143. package/src/hooks.test.ts +168 -0
  144. package/src/hooks.ts +26 -0
  145. package/src/index.ts +29 -0
  146. package/src/model-context/injectInteractableContext.test.ts +167 -0
  147. package/src/model-context/injectInteractableContext.ts +64 -0
  148. package/src/model-context/injectQuoteContext.test.ts +33 -0
  149. package/src/model-context/injectQuoteContext.ts +63 -0
  150. package/src/runtime/AISDKChat.artifacts.test.ts +48 -0
  151. package/src/runtime/AISDKChat.integration.test.tsx +148 -0
  152. package/src/runtime/AISDKChat.test.ts +241 -0
  153. package/src/runtime/AISDKChat.ts +43 -0
  154. package/src/runtime/AISDKThreads.test.ts +811 -0
  155. package/src/runtime/AISDKThreads.ts +194 -0
  156. package/src/runtime/DynamicChatTransport.test.ts +228 -0
  157. package/src/runtime/DynamicChatTransport.ts +281 -0
  158. package/src/runtime/__tests__/controlled-transport.ts +70 -0
  159. package/src/runtime/__tests__/tap.ts +80 -0
  160. package/src/runtime/__tests__/vue.ts +46 -0
  161. package/src/runtime/getResumableAdapter.ts +16 -0
  162. package/src/runtime/toolHistoryCodec.test.ts +187 -0
  163. package/src/runtime/toolHistoryCodec.ts +245 -0
  164. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1956 -0
  165. package/src/runtime/useAISDKRuntime.approval.test.tsx +580 -0
  166. package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
  167. package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +302 -0
  168. package/src/runtime/useAISDKRuntime.metadata-cache.test.tsx +157 -0
  169. package/src/runtime/useAISDKRuntime.queue.test.tsx +707 -0
  170. package/src/runtime/useAISDKRuntime.reload.test.tsx +217 -0
  171. package/src/runtime/useAISDKRuntime.test.ts +2345 -0
  172. package/src/runtime/useAISDKRuntime.tool-output.test.tsx +113 -0
  173. package/src/runtime/useAISDKRuntime.ts +1503 -0
  174. package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
  175. package/src/runtime/useChatOnTap.test.ts +60 -0
  176. package/src/runtime/useChatRuntime.fast-refresh.test.tsx +83 -0
  177. package/src/runtime/useChatRuntime.integration.test.tsx +661 -0
  178. package/src/runtime/useChatRuntime.local-storage.test.tsx +114 -0
  179. package/src/runtime/useChatRuntime.native.test.ts +147 -0
  180. package/src/runtime/useChatRuntime.run-telemetry.test.tsx +226 -0
  181. package/src/runtime/useChatRuntime.test.ts +619 -0
  182. package/src/runtime/useChatRuntime.ts +80 -0
  183. package/src/runtime/useChatSnapshot.test.ts +60 -0
  184. package/src/runtime/useChatSnapshot.ts +86 -0
  185. package/src/runtime/useChatThread.binding.test.tsx +140 -0
  186. package/src/runtime/useChatThread.test.ts +993 -0
  187. package/src/runtime/useChatThread.transport.test.tsx +149 -0
  188. package/src/runtime/useChatThread.ts +533 -0
  189. package/src/runtime/useDynamicChatTransport.ts +26 -0
  190. package/src/runtime/useExternalHistory.test.ts +4187 -0
  191. package/src/runtime/useExternalHistory.ts +902 -0
  192. package/src/runtime/useMessageQueue.effects.test.tsx +88 -0
  193. package/src/runtime/useMessageQueue.test.tsx +256 -0
  194. package/src/runtime/useMessageQueue.ts +189 -0
  195. package/src/runtime/useResourceCleanup.ts +58 -0
  196. package/src/runtime/useStreamingTiming.test.tsx +96 -0
  197. package/src/runtime/useStreamingTiming.ts +69 -0
  198. package/src/tools/generativeTools.test.ts +1058 -0
  199. package/src/tools/generativeTools.ts +491 -0
  200. package/src/tools/mcp-stdio.node.ts +1 -0
  201. package/src/tools/mcp-stdio.unsupported.ts +12 -0
  202. package/src/transport/AssistantChatTransport.test.ts +631 -0
  203. package/src/transport/AssistantChatTransport.ts +236 -0
  204. package/src/transport/resumable.test.ts +262 -0
  205. package/src/transport/resumable.ts +162 -0
  206. package/src/usage.test.ts +204 -0
  207. package/src/usage.ts +179 -0
  208. package/src/utils/getVercelAIMessages.ts +11 -0
  209. package/src/utils/sliceMessagesUntil.test.ts +51 -0
  210. package/src/utils/sliceMessagesUntil.ts +16 -0
@@ -0,0 +1,902 @@
1
+ "use client";
2
+
3
+ import type {
4
+ AssistantRuntime,
5
+ ThreadAssistantMessage,
6
+ ThreadHistoryAdapter,
7
+ ThreadMessage,
8
+ MessageFormatAdapter,
9
+ MessageFormatItem,
10
+ MessageFormatRepository,
11
+ ExportedMessageRepository,
12
+ RespondToToolApprovalOptions,
13
+ Unstable_ToolInteractionLog,
14
+ } from "@ulearning/ai-core";
15
+ import { getExternalStoreMessages } from "@ulearning/ai-core";
16
+ import {
17
+ MessageRepository,
18
+ withParentsFirst,
19
+ } from "@ulearning/ai-core/internal";
20
+ import { useAui } from "@ulearning/ai-store";
21
+ import {
22
+ useRef,
23
+ useEffect,
24
+ useState,
25
+ useCallback,
26
+ useMemo,
27
+ useSyncExternalStore,
28
+ } from "@ulearning/ai-tap";
29
+ import {
30
+ addToolData,
31
+ collectToolApprovalResponses,
32
+ collectToolArtifacts,
33
+ collectToolInteractions,
34
+ restoreToolData,
35
+ type StoredToolApprovalResponse,
36
+ } from "./toolHistoryCodec";
37
+
38
+ export const toExportedMessageRepository = <TMessage>(
39
+ toThreadMessages: (messages: TMessage[]) => ThreadMessage[],
40
+ messages: MessageFormatRepository<TMessage>,
41
+ ): ExportedMessageRepository => {
42
+ const converted = messages.messages.flatMap((m) => {
43
+ const message = toThreadMessages([m.message])[0];
44
+ if (!message) {
45
+ console.warn("Skipping a stored message that could not be loaded.");
46
+ return [];
47
+ }
48
+ return [{ ...m, message }];
49
+ });
50
+ const survivingIds = new Set<string>();
51
+ const survivors = withParentsFirst(converted, () => false).filter(
52
+ ({ parentId, message }) => {
53
+ if (parentId && !survivingIds.has(parentId)) return false;
54
+ survivingIds.add(message.id);
55
+ return true;
56
+ },
57
+ );
58
+
59
+ return {
60
+ headId:
61
+ messages.headId && survivingIds.has(messages.headId)
62
+ ? messages.headId
63
+ : null,
64
+ messages: survivors,
65
+ };
66
+ };
67
+
68
+ const hasUnansweredApproval = (message: ThreadMessage) =>
69
+ message.content.some(
70
+ (part) =>
71
+ part.type === "tool-call" &&
72
+ part.approval != null &&
73
+ part.approval.approved === undefined &&
74
+ part.approval.resolution === undefined,
75
+ );
76
+
77
+ // Core reports a run paused on an unanswered approval as an interrupt, not as tool calls.
78
+ const isAwaitingToolApproval = (message: ThreadMessage) =>
79
+ message.status?.type === "requires-action" &&
80
+ (message.status.reason === "tool-calls" ||
81
+ (message.status.reason === "interrupt" && hasUnansweredApproval(message)));
82
+
83
+ // A run that settles without an assistant message after its prompt leaves nothing to report on its own.
84
+ const isUnansweredRun = (
85
+ messages: readonly ThreadMessage[],
86
+ promptId: string,
87
+ ) => {
88
+ const promptIndex = messages.findIndex((message) => message.id === promptId);
89
+ return (
90
+ promptIndex === -1 ||
91
+ !messages
92
+ .slice(promptIndex + 1)
93
+ .some((message) => message.role === "assistant")
94
+ );
95
+ };
96
+
97
+ const createUnansweredRunMessage = (
98
+ reason: "cancelled" | "other",
99
+ ): ThreadAssistantMessage => ({
100
+ id: "unanswered-run",
101
+ role: "assistant",
102
+ content: [],
103
+ status: { type: "incomplete", reason },
104
+ createdAt: new Date(),
105
+ metadata: {
106
+ unstable_state: null,
107
+ unstable_annotations: [],
108
+ unstable_data: [],
109
+ steps: [],
110
+ custom: {},
111
+ },
112
+ });
113
+
114
+ const isTerminalMessage = (message: ThreadMessage) =>
115
+ message.status === undefined ||
116
+ message.status.type === "complete" ||
117
+ message.status.type === "incomplete";
118
+
119
+ const encodeContent = <TMessage>(
120
+ storageFormatAdapter: MessageFormatAdapter<TMessage, any>,
121
+ item: MessageFormatItem<TMessage>,
122
+ ) => JSON.stringify(storageFormatAdapter.encode(item));
123
+
124
+ export const useExternalHistory = <TMessage>(
125
+ runtimeRef: { current: AssistantRuntime },
126
+ historyAdapter: ThreadHistoryAdapter | undefined,
127
+ toThreadMessages: (messages: TMessage[]) => ThreadMessage[],
128
+ storageFormatAdapter: MessageFormatAdapter<TMessage, any>,
129
+ onSetMessages: (messages: TMessage[]) => void,
130
+ getToolArtifacts?: () => ReadonlyMap<string, unknown>,
131
+ onToolArtifactsRestored?: (next: Map<string, unknown>) => void,
132
+ getToolInteractions?: () => ReadonlyMap<string, Unstable_ToolInteractionLog>,
133
+ onToolInteractionsRestored?: (
134
+ next: Map<string, Unstable_ToolInteractionLog>,
135
+ ) => void,
136
+ toolApprovalResponses?: Map<string, RespondToToolApprovalOptions>,
137
+ onToolApprovalResponsesRestored?: () => void,
138
+ ) => {
139
+ const loadedRef = useRef(false);
140
+
141
+ const aui = useAui();
142
+ const optionalThreadListItem = useCallback(
143
+ () => (aui.threadListItem.source ? aui.threadListItem : null),
144
+ [aui],
145
+ );
146
+ const getRemoteId = useCallback(
147
+ () => optionalThreadListItem()?.getState().remoteId,
148
+ [optionalThreadListItem],
149
+ );
150
+ const remoteId = useSyncExternalStore(
151
+ aui.subscribe,
152
+ getRemoteId,
153
+ getRemoteId,
154
+ );
155
+
156
+ const [hasLoaded, setHasLoaded] = useState(false);
157
+ const historyReadyRef = useRef(false);
158
+ const deferredPersistRef = useRef(false);
159
+ const persistWhenReadyRef = useRef<() => void>(() => {});
160
+ const loadInFlightRef = useRef<Promise<void> | null>(null);
161
+
162
+ useEffect(() => {
163
+ historyReadyRef.current = hasLoaded;
164
+ if (hasLoaded) persistWhenReadyRef.current();
165
+ }, [hasLoaded]);
166
+
167
+ const historyIds = useRef(new Set<string>());
168
+ const deferredTelemetryIds = useRef(new Set<string>());
169
+ // A deleted message stays in the chat until its storage delete resolves, so passes queued before it leaves skip it, and its release is queued behind them.
170
+ const deletedMessageIds = useRef(new Map<string, boolean>());
171
+ // `content` is a snapshot taken at write time rather than a re-encode of `source`, because a retained message object can be mutated in place by the runtime that produced it.
172
+ const persistedInnerMessages = useRef(
173
+ new Map<string, { source: TMessage; content: string }>(),
174
+ );
175
+
176
+ const onSetMessagesRef = useRef(onSetMessages);
177
+ useEffect(() => {
178
+ onSetMessagesRef.current = onSetMessages;
179
+ });
180
+
181
+ const formatAdapter = useMemo(() => {
182
+ if (!historyAdapter) return undefined;
183
+ if (!historyAdapter.withFormat) {
184
+ throw new Error(
185
+ "useAISDKRuntime: ThreadHistoryAdapter is missing the required `withFormat` method.",
186
+ );
187
+ }
188
+ return historyAdapter.withFormat<TMessage, any>(storageFormatAdapter);
189
+ }, [historyAdapter, storageFormatAdapter]);
190
+
191
+ const isLoading = formatAdapter != null && !hasLoaded;
192
+
193
+ useEffect(() => {
194
+ if (!formatAdapter || loadedRef.current) return undefined;
195
+
196
+ const loadHistory = async () => {
197
+ try {
198
+ const repo = await formatAdapter.load();
199
+ if (optionalThreadListItem()?.getState().remoteId !== remoteId) return;
200
+ // A resumed stream or a send may have appended live messages while
201
+ // the load was in flight. The history imports beneath that live tail
202
+ // so neither side of the race is dropped.
203
+ const liveMessages = runtimeRef.current.thread.getState().messages;
204
+ const restoredArtifacts = new Map(
205
+ liveMessages.length > 0 ? getToolArtifacts?.() : undefined,
206
+ );
207
+ const restoredInteractions = new Map(
208
+ liveMessages.length > 0 ? getToolInteractions?.() : undefined,
209
+ );
210
+ if (liveMessages.length === 0) {
211
+ toolApprovalResponses?.clear();
212
+ }
213
+ const restoredMessages =
214
+ repo?.messages.map((item) => ({
215
+ ...item,
216
+ message: restoreToolData(
217
+ item.message,
218
+ restoredArtifacts,
219
+ restoredInteractions,
220
+ toolApprovalResponses,
221
+ ),
222
+ })) ?? [];
223
+ onToolArtifactsRestored?.(restoredArtifacts);
224
+ onToolInteractionsRestored?.(restoredInteractions);
225
+ onToolApprovalResponsesRestored?.();
226
+ if (repo && restoredMessages.length > 0) {
227
+ const restoredRepo = { ...repo, messages: restoredMessages };
228
+ for (const [index, m] of repo.messages.entries()) {
229
+ persistedInnerMessages.current.set(
230
+ storageFormatAdapter.getId(m.message),
231
+ {
232
+ source: restoredMessages[index]!.message,
233
+ content: encodeContent(storageFormatAdapter, m),
234
+ },
235
+ );
236
+ }
237
+ const converted = toExportedMessageRepository(
238
+ toThreadMessages,
239
+ restoredRepo,
240
+ );
241
+ const liveById = new Map(
242
+ liveMessages.map((message) => [message.id, message]),
243
+ );
244
+ const loadedMessages = converted.messages.map((item) => ({
245
+ ...item,
246
+ message: liveById.get(item.message.id) ?? item.message,
247
+ }));
248
+ const loadedIds = new Set(loadedMessages.map((m) => m.message.id));
249
+ const liveTail = liveMessages.filter((m) => !loadedIds.has(m.id));
250
+ const anchorId =
251
+ converted.headId ?? converted.messages.at(-1)?.message.id ?? null;
252
+ const merged =
253
+ liveTail.length === 0
254
+ ? { ...converted, messages: loadedMessages }
255
+ : {
256
+ headId: liveTail.at(-1)!.id,
257
+ messages: [
258
+ ...loadedMessages,
259
+ ...liveTail.map((message, index) => ({
260
+ parentId:
261
+ index === 0 ? anchorId : liveTail[index - 1]!.id,
262
+ message,
263
+ })),
264
+ ],
265
+ };
266
+ runtimeRef.current.thread.import(merged);
267
+
268
+ const tempRepo = new MessageRepository();
269
+ tempRepo.import(merged);
270
+ const messages = tempRepo.getMessages();
271
+
272
+ onSetMessagesRef.current(
273
+ messages.flatMap(getExternalStoreMessages<TMessage>),
274
+ );
275
+
276
+ historyIds.current = new Set();
277
+ for (const m of loadedMessages) {
278
+ historyIds.current.add(m.message.id);
279
+ if (isAwaitingToolApproval(m.message)) {
280
+ deferredTelemetryIds.current.add(m.message.id);
281
+ }
282
+ }
283
+ }
284
+ } catch (error) {
285
+ console.error("Failed to load message history:", error);
286
+ } finally {
287
+ setHasLoaded(true);
288
+ }
289
+ };
290
+
291
+ formatAdapter.pin?.();
292
+
293
+ if (!remoteId) {
294
+ // eslint-disable-next-line react-hooks/set-state-in-effect
295
+ setHasLoaded(true);
296
+ return undefined;
297
+ }
298
+
299
+ const threadState = runtimeRef.current.thread.getState();
300
+ if (threadState.isRunning || threadState.messages.length > 0) {
301
+ loadedRef.current = true;
302
+ setHasLoaded(true);
303
+ return undefined;
304
+ }
305
+
306
+ loadedRef.current = true;
307
+ historyReadyRef.current = false;
308
+ setHasLoaded(false);
309
+ loadInFlightRef.current = loadHistory();
310
+ return undefined;
311
+ }, [
312
+ formatAdapter,
313
+ toThreadMessages,
314
+ runtimeRef,
315
+ optionalThreadListItem,
316
+ remoteId,
317
+ storageFormatAdapter,
318
+ getToolArtifacts,
319
+ onToolArtifactsRestored,
320
+ getToolInteractions,
321
+ onToolInteractionsRestored,
322
+ toolApprovalResponses,
323
+ onToolApprovalResponsesRestored,
324
+ ]);
325
+
326
+ const runStartRef = useRef<number | null>(null);
327
+ const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
328
+ const persistInFlightRef = useRef<Promise<void>>(Promise.resolve());
329
+ const stepBoundariesRef = useRef<number[]>([]);
330
+ const wasRunningRef = useRef(false);
331
+ const toolCallCountRef = useRef(0);
332
+ const queuedPromptIdRef = useRef<string | null>(null);
333
+ const runPromptIdRef = useRef<string | null>(null);
334
+ const runPromptMessagesRef = useRef<TMessage[]>([]);
335
+ const settledPromptIdRef = useRef<string | null>(null);
336
+ const settledPromptMessagesRef = useRef<TMessage[]>([]);
337
+ const runCancelledRef = useRef(false);
338
+ const settledRunCancelledRef = useRef(false);
339
+
340
+ const markRunCancelled = useCallback(() => {
341
+ if (wasRunningRef.current) runCancelledRef.current = true;
342
+ }, []);
343
+
344
+ const releaseRemovedMessages = useCallback(
345
+ (messages: readonly ThreadMessage[]) => {
346
+ for (const [messageId, releaseQueued] of deletedMessageIds.current) {
347
+ if (
348
+ releaseQueued ||
349
+ messages.some((message) => message.id === messageId)
350
+ )
351
+ continue;
352
+ deletedMessageIds.current.set(messageId, true);
353
+ persistInFlightRef.current = persistInFlightRef.current.then(() => {
354
+ deletedMessageIds.current.delete(messageId);
355
+ });
356
+ }
357
+ },
358
+ [],
359
+ );
360
+
361
+ const clearDeletedMessage = useCallback(
362
+ (messageId: string, items: MessageFormatItem<TMessage>[]) => {
363
+ historyIds.current.delete(messageId);
364
+ deferredTelemetryIds.current.delete(messageId);
365
+ deletedMessageIds.current.set(messageId, false);
366
+ releaseRemovedMessages(runtimeRef.current.thread.getState().messages);
367
+ for (const item of items) {
368
+ persistedInnerMessages.current.delete(
369
+ storageFormatAdapter.getId(item.message),
370
+ );
371
+ }
372
+ },
373
+ [releaseRemovedMessages, runtimeRef, storageFormatAdapter],
374
+ );
375
+
376
+ const persistToolInteractions = useCallback(
377
+ (messageId: string) => {
378
+ const persistence = persistInFlightRef.current.then(async () => {
379
+ if (!formatAdapter?.update) return;
380
+ const messages = runtimeRef.current.thread.getState().messages;
381
+ const message = messages.find(
382
+ (item) =>
383
+ item.id === messageId ||
384
+ getExternalStoreMessages<TMessage>(item).some(
385
+ (innerMessage) =>
386
+ storageFormatAdapter.getId(innerMessage) === messageId,
387
+ ),
388
+ );
389
+ if (!message) return;
390
+
391
+ const previousMessages = messages
392
+ .slice(0, messages.indexOf(message))
393
+ .filter((item) => !deletedMessageIds.current.has(item.id))
394
+ .flatMap(getExternalStoreMessages<TMessage>);
395
+ let parentId = previousMessages.at(-1)
396
+ ? storageFormatAdapter.getId(previousMessages.at(-1)!)
397
+ : null;
398
+ const storedToolArtifacts = collectToolArtifacts(
399
+ message,
400
+ getToolArtifacts?.(),
401
+ );
402
+ const storedToolInteractions = collectToolInteractions(
403
+ message,
404
+ getToolInteractions?.(),
405
+ );
406
+ const storedToolApprovalResponses = collectToolApprovalResponses(
407
+ message,
408
+ toolApprovalResponses,
409
+ );
410
+
411
+ for (const innerMessage of getExternalStoreMessages<TMessage>(
412
+ message,
413
+ )) {
414
+ const item = {
415
+ parentId,
416
+ message: addToolData(
417
+ innerMessage,
418
+ storedToolArtifacts,
419
+ storedToolInteractions,
420
+ storedToolApprovalResponses,
421
+ ),
422
+ };
423
+ const innerId = storageFormatAdapter.getId(item.message);
424
+ const persisted = persistedInnerMessages.current.get(innerId);
425
+ if (persisted) {
426
+ const content = encodeContent(storageFormatAdapter, item);
427
+ if (content === persisted.content) {
428
+ persisted.source = item.message;
429
+ } else {
430
+ await formatAdapter.update(item, innerId);
431
+ persistedInnerMessages.current.set(innerId, {
432
+ source: item.message,
433
+ content,
434
+ });
435
+ }
436
+ }
437
+ parentId = innerId;
438
+ }
439
+ });
440
+ persistInFlightRef.current = persistence.catch(() => {});
441
+ return persistence.catch((error) => {
442
+ console.error("Failed to persist tool data:", error);
443
+ });
444
+ },
445
+ [
446
+ formatAdapter,
447
+ runtimeRef,
448
+ storageFormatAdapter,
449
+ getToolArtifacts,
450
+ getToolInteractions,
451
+ toolApprovalResponses,
452
+ ],
453
+ );
454
+
455
+ useEffect(() => {
456
+ if (!formatAdapter) return;
457
+ const adapter = formatAdapter;
458
+
459
+ const idleScheduledInnerIds = new Set<string>();
460
+ const unpersistedInnerIds = (message: ThreadMessage) =>
461
+ isTerminalMessage(message)
462
+ ? getExternalStoreMessages<TMessage>(message)
463
+ .map((innerMessage) => storageFormatAdapter.getId(innerMessage))
464
+ .filter((innerId) => !persistedInnerMessages.current.has(innerId))
465
+ : [];
466
+
467
+ const unsubscribe = runtimeRef.current.thread.subscribe(() => {
468
+ const threadState = runtimeRef.current.thread.getState();
469
+ releaseRemovedMessages(threadState.messages);
470
+ const { isRunning } = threadState;
471
+ const wasRunning = wasRunningRef.current;
472
+ wasRunningRef.current = isRunning;
473
+
474
+ // Track step boundaries by content changes (more reliable than isRunning)
475
+ if (runStartRef.current != null) {
476
+ const lastMsg = threadState.messages.at(-1);
477
+ if (lastMsg?.role === "assistant") {
478
+ const currentToolCallCount = lastMsg.content.filter(
479
+ (p) => p.type === "tool-call",
480
+ ).length;
481
+ while (toolCallCountRef.current < currentToolCallCount) {
482
+ stepBoundariesRef.current.push(Date.now() - runStartRef.current);
483
+ toolCallCountRef.current++;
484
+ }
485
+ }
486
+ }
487
+
488
+ if (isRunning) {
489
+ if (runStartRef.current == null) {
490
+ runStartRef.current = Date.now();
491
+ stepBoundariesRef.current = [];
492
+ toolCallCountRef.current = 0;
493
+ adapter.pin?.();
494
+ }
495
+ const prompt = threadState.messages.findLast(
496
+ (message) => message.role === "user",
497
+ );
498
+ if (prompt) {
499
+ runPromptIdRef.current = prompt.id;
500
+ runPromptMessagesRef.current =
501
+ getExternalStoreMessages<TMessage>(prompt);
502
+ }
503
+ if (historyReadyRef.current) persistRunPrompt(threadState.messages);
504
+ // Cancel any pending persist — isRunning went back to true
505
+ if (persistTimerRef.current) {
506
+ clearTimeout(persistTimerRef.current);
507
+ persistTimerRef.current = null;
508
+ }
509
+ return;
510
+ }
511
+
512
+ if (wasRunning) {
513
+ settledPromptIdRef.current = runPromptIdRef.current;
514
+ settledPromptMessagesRef.current = runPromptMessagesRef.current;
515
+ settledRunCancelledRef.current = runCancelledRef.current;
516
+ runPromptIdRef.current = null;
517
+ runPromptMessagesRef.current = [];
518
+ runCancelledRef.current = false;
519
+ queuedPromptIdRef.current = null;
520
+ // Record step boundary offset (synchronous for accuracy)
521
+ if (runStartRef.current != null) {
522
+ stepBoundariesRef.current.push(Date.now() - runStartRef.current);
523
+ }
524
+ } else {
525
+ const pending = threadState.messages.flatMap(unpersistedInnerIds);
526
+ if (pending.every((innerId) => idleScheduledInnerIds.has(innerId)))
527
+ return;
528
+ for (const innerId of pending) idleScheduledInnerIds.add(innerId);
529
+ }
530
+
531
+ // Debounce: wait one macrotask so agentic step flickers are absorbed
532
+ if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
533
+ persistTimerRef.current = setTimeout(() => {
534
+ persistSettled(false);
535
+ }, 0);
536
+ });
537
+
538
+ const initialThreadState = runtimeRef.current.thread.getState();
539
+ wasRunningRef.current = initialThreadState.isRunning;
540
+ if (initialThreadState.isRunning) {
541
+ if (runStartRef.current == null) {
542
+ runStartRef.current = Date.now();
543
+ stepBoundariesRef.current = [];
544
+ toolCallCountRef.current = 0;
545
+ adapter.pin?.();
546
+ }
547
+ } else if (initialThreadState.messages.length > 0) {
548
+ persistSettled(true);
549
+ }
550
+
551
+ function persistSettled(
552
+ ignoreRunning: boolean,
553
+ ignoreHistoryReady = false,
554
+ ) {
555
+ persistTimerRef.current = null;
556
+ if (!historyReadyRef.current && !ignoreHistoryReady) {
557
+ deferredPersistRef.current = true;
558
+ return;
559
+ }
560
+ const latest = runtimeRef.current.thread.getState();
561
+ if (!ignoreRunning && latest.isRunning) return;
562
+ deferredPersistRef.current = false;
563
+
564
+ const boundaries = stepBoundariesRef.current;
565
+ const durationMs = boundaries.length > 0 ? boundaries.at(-1) : undefined;
566
+
567
+ // Fallback: if only 1 boundary but message has multiple steps, distribute evenly
568
+ if (boundaries.length === 1 && durationMs != null) {
569
+ const lastAssistant = latest.messages.findLast(
570
+ (m) => m.role === "assistant",
571
+ );
572
+ if (lastAssistant) {
573
+ const tcCount = lastAssistant.content.filter(
574
+ (p) => p.type === "tool-call",
575
+ ).length;
576
+ if (tcCount > 0) {
577
+ const totalSteps = tcCount + 1;
578
+ const stepDur = durationMs / totalSteps;
579
+ boundaries.length = 0;
580
+ for (let i = 0; i < totalSteps; i++) {
581
+ boundaries.push(Math.round((i + 1) * stepDur));
582
+ }
583
+ }
584
+ }
585
+ }
586
+
587
+ // Build per-step timestamps when there are multiple steps
588
+ const stepTimestamps =
589
+ boundaries.length > 1
590
+ ? boundaries.map((endMs, i) => ({
591
+ start_ms: i === 0 ? 0 : boundaries[i - 1]!,
592
+ end_ms: endMs,
593
+ }))
594
+ : undefined;
595
+
596
+ runStartRef.current = null;
597
+ stepBoundariesRef.current = [];
598
+ const promptId = settledPromptIdRef.current;
599
+ const promptMessages = settledPromptMessagesRef.current;
600
+ const runCancelled = settledRunCancelledRef.current;
601
+ settledPromptIdRef.current = null;
602
+ settledPromptMessagesRef.current = [];
603
+ settledRunCancelledRef.current = false;
604
+
605
+ const telemetryOptions = {
606
+ ...(durationMs != null ? { durationMs } : undefined),
607
+ ...(stepTimestamps != null ? { stepTimestamps } : undefined),
608
+ };
609
+
610
+ persistInFlightRef.current = persistInFlightRef.current
611
+ .then(async () => {
612
+ const { messages } = latest;
613
+ let lastInnerMessageId: string | null = null;
614
+ let lastUserMessageId: string | undefined;
615
+ let writeFailure: { error: unknown } | undefined;
616
+
617
+ const getLastInnerId = (msgs: TMessage[]): string | null =>
618
+ msgs.length > 0 ? storageFormatAdapter.getId(msgs.at(-1)!) : null;
619
+
620
+ const toBatchItems = (
621
+ msgs: TMessage[],
622
+ toolArtifacts: Record<string, unknown> | undefined,
623
+ toolInteractions:
624
+ | Record<string, Unstable_ToolInteractionLog>
625
+ | undefined,
626
+ toolApprovalResponses:
627
+ | Record<string, StoredToolApprovalResponse>
628
+ | undefined,
629
+ ) =>
630
+ msgs.map((msg, idx) => ({
631
+ parentId:
632
+ idx === 0
633
+ ? lastInnerMessageId
634
+ : storageFormatAdapter.getId(msgs[idx - 1]!),
635
+ message: addToolData(
636
+ msg,
637
+ toolArtifacts,
638
+ toolInteractions,
639
+ toolApprovalResponses,
640
+ ),
641
+ }));
642
+
643
+ for (const message of messages) {
644
+ if (deletedMessageIds.current.has(message.id)) continue;
645
+ if (message.role === "user") lastUserMessageId = message.id;
646
+ const innerMessages = getExternalStoreMessages<TMessage>(message);
647
+
648
+ const isTerminal = isTerminalMessage(message);
649
+ const isAwaitingToolCalls = isAwaitingToolApproval(message);
650
+ // A paused message's later content can only reach storage via update, so it is persisted early only when the adapter supports update.
651
+ const isReady =
652
+ isTerminal ||
653
+ (isAwaitingToolCalls && adapter.update !== undefined);
654
+
655
+ if (!isReady) {
656
+ lastInnerMessageId =
657
+ getLastInnerId(innerMessages) ?? lastInnerMessageId;
658
+ continue;
659
+ }
660
+
661
+ if (!historyIds.current.has(message.id)) {
662
+ historyIds.current.add(message.id);
663
+ deferredTelemetryIds.current.add(message.id);
664
+ }
665
+
666
+ const batchItems = toBatchItems(
667
+ innerMessages,
668
+ collectToolArtifacts(message, getToolArtifacts?.()),
669
+ collectToolInteractions(message, getToolInteractions?.()),
670
+ collectToolApprovalResponses(message, toolApprovalResponses),
671
+ );
672
+ let messageWriteFailure = writeFailure;
673
+ for (const item of batchItems) {
674
+ // Rows are stored parent first, so nothing after a failed append can be stored in this pass.
675
+ if (writeFailure) break;
676
+ const innerId = storageFormatAdapter.getId(item.message);
677
+ const persisted = persistedInnerMessages.current.get(innerId);
678
+ if (!persisted) {
679
+ try {
680
+ await appendIfUnpersisted(item);
681
+ } catch (error) {
682
+ console.error("Failed to persist message history:", error);
683
+ writeFailure = { error };
684
+ messageWriteFailure = writeFailure;
685
+ }
686
+ } else if (
687
+ persisted.source !== item.message &&
688
+ durationMs !== undefined &&
689
+ adapter.update
690
+ ) {
691
+ const content = encodeContent(storageFormatAdapter, item);
692
+ if (content === persisted.content) {
693
+ persisted.source = item.message;
694
+ } else {
695
+ try {
696
+ await adapter.update(item, innerId);
697
+ persistedInnerMessages.current.set(innerId, {
698
+ source: item.message,
699
+ content,
700
+ });
701
+ } catch (error) {
702
+ // A failed update leaves the stale baseline behind so the next run stop retries it.
703
+ messageWriteFailure ??= { error };
704
+ }
705
+ }
706
+ }
707
+ }
708
+
709
+ lastInnerMessageId =
710
+ getLastInnerId(innerMessages) ?? lastInnerMessageId;
711
+
712
+ if (deferredTelemetryIds.current.has(message.id) && isTerminal) {
713
+ deferredTelemetryIds.current.delete(message.id);
714
+ adapter.reportTelemetry?.(
715
+ batchItems.map((item, index) => ({
716
+ ...item,
717
+ message: innerMessages[index]!,
718
+ })),
719
+ {
720
+ ...telemetryOptions,
721
+ ...(lastUserMessageId !== undefined
722
+ ? { userMessageId: lastUserMessageId }
723
+ : undefined),
724
+ message,
725
+ ...(messageWriteFailure && {
726
+ persistenceError: messageWriteFailure.error,
727
+ }),
728
+ },
729
+ );
730
+ }
731
+ }
732
+
733
+ if (promptId !== null && isUnansweredRun(messages, promptId)) {
734
+ const shouldDeletePrompt =
735
+ runCancelled &&
736
+ !messages.some((message) => message.id === promptId);
737
+ adapter.reportTelemetry?.([], {
738
+ ...telemetryOptions,
739
+ ...(!(shouldDeletePrompt && adapter.delete)
740
+ ? { userMessageId: promptId }
741
+ : undefined),
742
+ message: createUnansweredRunMessage(
743
+ runCancelled ? "cancelled" : "other",
744
+ ),
745
+ });
746
+ if (shouldDeletePrompt && adapter.delete) {
747
+ const items = promptMessages.map((message) => ({
748
+ parentId: null,
749
+ message,
750
+ }));
751
+ try {
752
+ await adapter.delete(items);
753
+ clearDeletedMessage(promptId, items);
754
+ } catch (error) {
755
+ console.error("Failed to delete message history:", error);
756
+ }
757
+ }
758
+ }
759
+ })
760
+ .catch((error) => {
761
+ console.error("Failed to persist message history:", error);
762
+ });
763
+ }
764
+
765
+ persistWhenReadyRef.current = () => {
766
+ const state = runtimeRef.current.thread.getState();
767
+ if (state.messages.length === 0 && settledPromptIdRef.current === null)
768
+ return;
769
+ if (state.isRunning) {
770
+ persistRunPrompt(state.messages);
771
+ return;
772
+ }
773
+ if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
774
+ persistSettled(true);
775
+ };
776
+
777
+ function persistRunPrompt(messages: readonly ThreadMessage[]) {
778
+ const promptIndex = messages.findLastIndex(
779
+ (message) => message.role === "user",
780
+ );
781
+ const prompt = messages[promptIndex];
782
+ if (!prompt || prompt.id === queuedPromptIdRef.current) return;
783
+ queuedPromptIdRef.current = prompt.id;
784
+ const innerMessages = getExternalStoreMessages<TMessage>(prompt);
785
+ if (
786
+ innerMessages.every((innerMessage) =>
787
+ persistedInnerMessages.current.has(
788
+ storageFormatAdapter.getId(innerMessage),
789
+ ),
790
+ )
791
+ )
792
+ return;
793
+ const previousInnerMessage = messages
794
+ .slice(0, promptIndex)
795
+ .flatMap(getExternalStoreMessages<TMessage>)
796
+ .at(-1);
797
+
798
+ const persistence = persistInFlightRef.current.then(async () => {
799
+ if (deletedMessageIds.current.has(prompt.id)) return;
800
+ let parentId = previousInnerMessage
801
+ ? storageFormatAdapter.getId(previousInnerMessage)
802
+ : null;
803
+ for (const innerMessage of innerMessages) {
804
+ // Rows are appended parent first, so a prompt whose parent is not stored yet waits for the settled pass.
805
+ if (
806
+ parentId !== null &&
807
+ !persistedInnerMessages.current.has(parentId)
808
+ )
809
+ return;
810
+ await appendIfUnpersisted({ parentId, message: innerMessage });
811
+ parentId = storageFormatAdapter.getId(innerMessage);
812
+ }
813
+ });
814
+ persistInFlightRef.current = persistence.catch((error) => {
815
+ console.error("Failed to persist message history:", error);
816
+ });
817
+ }
818
+
819
+ async function appendIfUnpersisted(item: MessageFormatItem<TMessage>) {
820
+ const innerId = storageFormatAdapter.getId(item.message);
821
+ if (persistedInnerMessages.current.has(innerId)) return;
822
+ await adapter.append(item);
823
+ persistedInnerMessages.current.set(innerId, {
824
+ source: item.message,
825
+ content: encodeContent(storageFormatAdapter, item),
826
+ });
827
+ }
828
+
829
+ return () => {
830
+ persistWhenReadyRef.current = () => {};
831
+ unsubscribe();
832
+ const hasPendingTimer = persistTimerRef.current !== null;
833
+ if (persistTimerRef.current) {
834
+ clearTimeout(persistTimerRef.current);
835
+ persistTimerRef.current = null;
836
+ }
837
+ if (
838
+ !historyReadyRef.current &&
839
+ (deferredPersistRef.current || hasPendingTimer)
840
+ ) {
841
+ if (loadInFlightRef.current) {
842
+ void loadInFlightRef.current.then(() => persistSettled(true, true));
843
+ } else {
844
+ persistSettled(true, true);
845
+ }
846
+ } else if (hasPendingTimer) {
847
+ persistSettled(false);
848
+ }
849
+ };
850
+ }, [
851
+ formatAdapter,
852
+ storageFormatAdapter,
853
+ runtimeRef,
854
+ getToolArtifacts,
855
+ getToolInteractions,
856
+ toolApprovalResponses,
857
+ releaseRemovedMessages,
858
+ clearDeletedMessage,
859
+ ]);
860
+
861
+ const deleteMessage = useCallback(
862
+ async (messageId: string) => {
863
+ const deleteMessages = formatAdapter?.delete?.bind(formatAdapter);
864
+ if (!deleteMessages) return;
865
+
866
+ const messages = runtimeRef.current.thread.getState().messages;
867
+ const messageIndex = messages.findIndex((m) => m.id === messageId);
868
+ if (messageIndex === -1) return;
869
+
870
+ const previousInnerMessages = messages
871
+ .slice(0, messageIndex)
872
+ .flatMap(getExternalStoreMessages<TMessage>);
873
+ let parentId = previousInnerMessages.at(-1)
874
+ ? storageFormatAdapter.getId(previousInnerMessages.at(-1)!)
875
+ : null;
876
+ const itemsToDelete = getExternalStoreMessages<TMessage>(
877
+ messages[messageIndex]!,
878
+ ).map((message) => {
879
+ const item = { parentId, message };
880
+ parentId = storageFormatAdapter.getId(message);
881
+ return item;
882
+ });
883
+
884
+ const deletion = persistInFlightRef.current.then(async () => {
885
+ await deleteMessages(itemsToDelete);
886
+ clearDeletedMessage(messageId, itemsToDelete);
887
+ });
888
+
889
+ persistInFlightRef.current = deletion.catch(() => {});
890
+ await deletion;
891
+ },
892
+ [formatAdapter, runtimeRef, storageFormatAdapter, clearDeletedMessage],
893
+ );
894
+
895
+ return {
896
+ isLoading,
897
+ deleteMessage,
898
+ markRunCancelled,
899
+ persistToolInteractions,
900
+ persistToolApprovalResponses: persistToolInteractions,
901
+ };
902
+ };