@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,919 @@
1
+ import {
2
+ isToolUIPart,
3
+ isReasoningFileUIPart,
4
+ isCustomContentUIPart,
5
+ getToolName,
6
+ type FileUIPart,
7
+ type UIMessage,
8
+ } from "ai";
9
+ import {
10
+ createMessageConverter as unstable_createMessageConverter,
11
+ type useExternalMessageConverter,
12
+ } from "@ulearning/ai-core/vue";
13
+ import {
14
+ isMcpAppUri,
15
+ type ReasoningMessagePart,
16
+ type ToolApprovalOption,
17
+ type ToolApprovalQuestion,
18
+ type ToolApprovalQuestionOption,
19
+ type ToolCallMessagePart,
20
+ type TextMessagePart,
21
+ type DataMessagePart,
22
+ type PartProviderMetadata,
23
+ type SourceMessagePart,
24
+ type SourceProviderMetadata,
25
+ type FileMessagePart,
26
+ type ThreadMessageLike,
27
+ type McpAppMetadata,
28
+ type MessagePartStreamStatus,
29
+ type RespondToToolApprovalOptions,
30
+ type Unstable_ToolInteractionLog,
31
+ } from "@ulearning/ai-core";
32
+ import { normalizeToolApprovalAnswers } from "./toolApprovalAnswers";
33
+ import {
34
+ createExternalMessageMetadataKey,
35
+ shallowArrayEqual,
36
+ stableStringifyToolArgs,
37
+ } from "@ulearning/ai-core/internal";
38
+ import { markPartialJsonObjectComplete } from "@ulearning/ai-stream/internal";
39
+ import {
40
+ parsePartialJsonObject,
41
+ type ReadonlyJSONObject,
42
+ } from "@ulearning/ai-stream/utils";
43
+ import { unwrapModelContentEnvelope } from "@ulearning/ai-stream/internal";
44
+
45
+ type MessageMetadata = ThreadMessageLike["metadata"];
46
+
47
+ const THREAD_METADATA_KEYS = new Set([
48
+ "unstable_state",
49
+ "unstable_annotations",
50
+ "unstable_data",
51
+ "steps",
52
+ "timing",
53
+ "submittedFeedback",
54
+ "isOptimistic",
55
+ "modality",
56
+ "custom",
57
+ ]);
58
+
59
+ const toThreadMetadata = (metadata: unknown): MessageMetadata => {
60
+ if (!metadata || typeof metadata !== "object") return undefined;
61
+ const result: Record<string, unknown> = {};
62
+ const extra = Object.create(null) as Record<string, unknown>;
63
+ for (const [key, value] of Object.entries(metadata)) {
64
+ (THREAD_METADATA_KEYS.has(key) ? result : extra)[key] = value;
65
+ }
66
+ if (Object.keys(extra).length > 0) {
67
+ result.custom = { ...extra, ...(result.custom as object | undefined) };
68
+ }
69
+ return result as MessageMetadata;
70
+ };
71
+ export type AISDKMessageConverterMetadata =
72
+ useExternalMessageConverter.Metadata & {
73
+ createdAtCache?: Map<string, Date>;
74
+ toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;
75
+ /**
76
+ * Frozen text and completion-marked args keyed weakly by a settled tool
77
+ * call's input object, then by call, since the text carries its streamed key
78
+ * order. A known call/input pair skips serialization and parsing; entries
79
+ * become collectible once the input is unreachable.
80
+ */
81
+ toolArgsTextCache?: WeakMap<
82
+ ReadonlyJSONObject,
83
+ Map<string, { argsText: string; args: ReadonlyJSONObject }>
84
+ >;
85
+ toolLastInputCache?: Map<string, ReadonlyJSONObject>;
86
+ mcpAppMetadataCache?: Map<string, McpAppMetadata>;
87
+ toolArtifacts?: ReadonlyMap<string, unknown>;
88
+ toolInteractions?: ReadonlyMap<string, Unstable_ToolInteractionLog>;
89
+ supportsRichToolApprovalResponses?: boolean;
90
+ toolApprovalResponses?: ReadonlyMap<string, RespondToToolApprovalOptions>;
91
+ cancelledToolApprovalIds?: ReadonlySet<string>;
92
+ cancelledStatusMessageIds?: ReadonlySet<string>;
93
+ /** Id of the currently-streaming message, flagged optimistic (#4037). */
94
+ optimisticMessageId?: string | undefined;
95
+ };
96
+
97
+ function stripClosingDelimiters(json: string): string {
98
+ let end = json.length;
99
+ while (end > 0 && '}]"'.includes(json[end - 1]!)) end--;
100
+ return json.slice(0, end);
101
+ }
102
+
103
+ const MCP_APP_METADATA_CACHE_MAX = 100;
104
+
105
+ function extractMcpAppMetadata(
106
+ part: unknown,
107
+ cache: Map<string, McpAppMetadata> | undefined,
108
+ ): McpAppMetadata | undefined {
109
+ if (!part || typeof part !== "object") return undefined;
110
+ const toolMetadata = (part as { toolMetadata?: unknown }).toolMetadata;
111
+ const toolApp =
112
+ toolMetadata && typeof toolMetadata === "object"
113
+ ? (toolMetadata as { app?: unknown }).app
114
+ : undefined;
115
+ const meta = (part as { callProviderMetadata?: unknown })
116
+ .callProviderMetadata;
117
+ const mcp =
118
+ meta && typeof meta === "object"
119
+ ? (meta as { mcp?: unknown }).mcp
120
+ : undefined;
121
+ const providerApp =
122
+ mcp && typeof mcp === "object" ? (mcp as { app?: unknown }).app : undefined;
123
+
124
+ const apps: Record<string, unknown>[] = [];
125
+ for (const candidate of [toolApp, providerApp]) {
126
+ if (!candidate || typeof candidate !== "object") continue;
127
+ const candidateApp = candidate as Record<string, unknown>;
128
+ const resourceUri = candidateApp["resourceUri"];
129
+ if (typeof resourceUri !== "string" || !isMcpAppUri(resourceUri)) continue;
130
+ apps.push({
131
+ resourceUri,
132
+ ...(typeof candidateApp["mimeType"] === "string" && {
133
+ mimeType: candidateApp["mimeType"],
134
+ }),
135
+ ...(Array.isArray(candidateApp["visibility"]) && {
136
+ visibility: candidateApp["visibility"],
137
+ }),
138
+ ...(candidate === providerApp &&
139
+ typeof candidateApp["serverId"] === "string" &&
140
+ candidateApp["serverId"].length > 0 && {
141
+ serverId: candidateApp["serverId"],
142
+ }),
143
+ });
144
+ }
145
+
146
+ let a: Record<string, unknown>;
147
+ if (apps.length > 0) {
148
+ a = { ...(apps[1] ?? {}), ...apps[0]! };
149
+ } else {
150
+ // MCP-UI tools surface the pointer on result._meta: canonical nested
151
+ // `ui.resourceUri`, or the deprecated flat `"ui/resourceUri"` key.
152
+ const output = (part as { output?: unknown }).output;
153
+ const outMeta =
154
+ output && typeof output === "object"
155
+ ? (output as { _meta?: unknown })._meta
156
+ : undefined;
157
+ const ui =
158
+ outMeta && typeof outMeta === "object"
159
+ ? (outMeta as Record<string, unknown>)["ui"]
160
+ : undefined;
161
+ if (
162
+ ui &&
163
+ typeof ui === "object" &&
164
+ typeof (ui as Record<string, unknown>)["resourceUri"] === "string" &&
165
+ isMcpAppUri((ui as Record<string, unknown>)["resourceUri"] as string)
166
+ ) {
167
+ // Only the spec'd ui fields cross from the result body; serverId is a
168
+ // routing key and stays transport-derived via callProviderMetadata.
169
+ const uiMeta = ui as Record<string, unknown>;
170
+ a = {
171
+ resourceUri: uiMeta["resourceUri"],
172
+ ...(Array.isArray(uiMeta["visibility"])
173
+ ? { visibility: uiMeta["visibility"] }
174
+ : {}),
175
+ };
176
+ } else {
177
+ const flat =
178
+ outMeta && typeof outMeta === "object"
179
+ ? (outMeta as Record<string, unknown>)["ui/resourceUri"]
180
+ : undefined;
181
+ if (typeof flat !== "string" || !isMcpAppUri(flat)) return undefined;
182
+ a = { resourceUri: flat };
183
+ }
184
+ }
185
+ if (typeof a["resourceUri"] !== "string") return undefined;
186
+ if (!isMcpAppUri(a["resourceUri"])) return undefined;
187
+ const cacheKey = `${typeof a["serverId"] === "string" ? a["serverId"] : ""} ${a["resourceUri"]}`;
188
+ const cached = cache?.get(cacheKey);
189
+ if (cached) {
190
+ cache!.delete(cacheKey);
191
+ cache!.set(cacheKey, cached);
192
+ return cached;
193
+ }
194
+ const out: { -readonly [K in keyof McpAppMetadata]: McpAppMetadata[K] } = {
195
+ resourceUri: a["resourceUri"],
196
+ };
197
+ if (typeof a["mimeType"] === "string") out.mimeType = a["mimeType"];
198
+ if (Array.isArray(a["visibility"])) {
199
+ out.visibility = a["visibility"].filter(
200
+ (v): v is "model" | "app" => v === "model" || v === "app",
201
+ );
202
+ }
203
+ if (typeof a["serverId"] === "string" && a["serverId"].length > 0)
204
+ out.serverId = a["serverId"];
205
+ if (cache) {
206
+ if (cache.size >= MCP_APP_METADATA_CACHE_MAX) {
207
+ const oldest = cache.keys().next().value;
208
+ if (oldest !== undefined) cache.delete(oldest);
209
+ }
210
+ cache.set(cacheKey, out);
211
+ }
212
+ return out;
213
+ }
214
+
215
+ const normalizeToolApprovalOptions = (
216
+ options: unknown,
217
+ ): readonly ToolApprovalOption[] | undefined => {
218
+ if (!Array.isArray(options)) return undefined;
219
+
220
+ return options.flatMap<ToolApprovalOption>((value) => {
221
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
222
+ const option = value as Record<string, unknown>;
223
+ if (typeof option.id !== "string" || typeof option.kind !== "string")
224
+ return [];
225
+
226
+ const confirm = option.confirm;
227
+ const confirmDetails =
228
+ confirm && typeof confirm === "object" && !Array.isArray(confirm)
229
+ ? (confirm as Record<string, unknown>)
230
+ : undefined;
231
+
232
+ return [
233
+ {
234
+ id: option.id,
235
+ kind: option.kind,
236
+ ...(typeof option.label === "string" && { label: option.label }),
237
+ ...(typeof option.description === "string" && {
238
+ description: option.description,
239
+ }),
240
+ ...(Array.isArray(option.grants) && {
241
+ grants: option.grants.filter(
242
+ (grant): grant is string => typeof grant === "string",
243
+ ),
244
+ }),
245
+ ...(typeof confirm === "boolean"
246
+ ? { confirm }
247
+ : confirmDetails
248
+ ? {
249
+ confirm: {
250
+ ...(typeof confirmDetails.title === "string" && {
251
+ title: confirmDetails.title,
252
+ }),
253
+ ...(typeof confirmDetails.description === "string" && {
254
+ description: confirmDetails.description,
255
+ }),
256
+ },
257
+ }
258
+ : {}),
259
+ },
260
+ ];
261
+ });
262
+ };
263
+
264
+ const normalizeToolApprovalQuestions = (
265
+ questions: unknown,
266
+ ): readonly ToolApprovalQuestion[] | undefined => {
267
+ if (!Array.isArray(questions)) return undefined;
268
+
269
+ return questions.flatMap<ToolApprovalQuestion>((value) => {
270
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
271
+ const question = value as Record<string, unknown>;
272
+ if (typeof question.id !== "string" || typeof question.prompt !== "string")
273
+ return [];
274
+ const options = Array.isArray(question.options)
275
+ ? question.options.flatMap<ToolApprovalQuestionOption>((option) => {
276
+ if (!option || typeof option !== "object" || Array.isArray(option))
277
+ return [];
278
+ const { id, label, description } = option as Record<string, unknown>;
279
+ if (typeof id !== "string" || typeof label !== "string") return [];
280
+ return [
281
+ {
282
+ id,
283
+ label,
284
+ ...(typeof description === "string" && { description }),
285
+ },
286
+ ];
287
+ })
288
+ : undefined;
289
+
290
+ return [
291
+ {
292
+ id: question.id,
293
+ prompt: question.prompt,
294
+ ...(typeof question.header === "string" && {
295
+ header: question.header,
296
+ }),
297
+ ...(options && { options }),
298
+ ...(typeof question.multiple === "boolean" && {
299
+ multiple: question.multiple,
300
+ }),
301
+ ...(typeof question.allowFreeform === "boolean" && {
302
+ allowFreeform: question.allowFreeform,
303
+ }),
304
+ },
305
+ ];
306
+ });
307
+ };
308
+
309
+ const APPROVAL_DESCRIPTOR_FIELDS = [
310
+ "prompt",
311
+ "display",
312
+ "allowFreeform",
313
+ "dismissible",
314
+ "options",
315
+ "optionId",
316
+ "text",
317
+ "questions",
318
+ "answers",
319
+ "resolution",
320
+ ] as const;
321
+
322
+ // The AI SDK's approval object declares none of the core request and answer
323
+ // fields and `validateUIMessages` strips unknown ones, so a host streams or
324
+ // persists them inside the opaque `approvalDescriptor`. Only those fields are
325
+ // read from it: a descriptor cannot approve its own request.
326
+ const readApprovalDescriptor = (
327
+ descriptor: unknown,
328
+ ): Record<string, unknown> => {
329
+ if (
330
+ !descriptor ||
331
+ typeof descriptor !== "object" ||
332
+ Array.isArray(descriptor)
333
+ )
334
+ return {};
335
+ const fields: Record<string, unknown> = {};
336
+ for (const key of APPROVAL_DESCRIPTOR_FIELDS) {
337
+ if (Object.hasOwn(descriptor, key))
338
+ fields[key] = (descriptor as Record<string, unknown>)[key];
339
+ }
340
+ return fields;
341
+ };
342
+
343
+ function getToolApprovalAndInterrupt(
344
+ part: {
345
+ approval?: Record<string, unknown> | undefined;
346
+ },
347
+ toolStatus: { type: string; payload?: unknown } | undefined,
348
+ supportsRichToolApprovalResponses: boolean,
349
+ toolApprovalResponses:
350
+ | ReadonlyMap<string, RespondToToolApprovalOptions>
351
+ | undefined,
352
+ cancelledToolApprovalIds: ReadonlySet<string> | undefined,
353
+ ): {
354
+ approval?: NonNullable<ToolCallMessagePart["approval"]>;
355
+ interrupt?: NonNullable<ToolCallMessagePart["interrupt"]>;
356
+ } {
357
+ if (part.approval) {
358
+ const rawApproval = {
359
+ ...readApprovalDescriptor(part.approval.descriptor),
360
+ ...part.approval,
361
+ };
362
+ const approval =
363
+ typeof rawApproval.id === "string" &&
364
+ rawApproval.approved === undefined &&
365
+ rawApproval.resolution === undefined &&
366
+ cancelledToolApprovalIds?.has(rawApproval.id)
367
+ ? { ...rawApproval, resolution: "cancelled" as const }
368
+ : rawApproval;
369
+ const response =
370
+ typeof approval.id === "string" &&
371
+ approval.approved === undefined &&
372
+ approval.resolution !== "cancelled" &&
373
+ approval.resolution !== "expired"
374
+ ? toolApprovalResponses?.get(approval.id)
375
+ : undefined;
376
+ // The built-in AI SDK channel sends only id, approved and reason back to
377
+ // the server, so a request shape promising any other answer would render
378
+ // controls whose response cannot travel.
379
+ const {
380
+ id,
381
+ prompt,
382
+ approved,
383
+ reason,
384
+ isAutomatic,
385
+ resolution,
386
+ display,
387
+ allowFreeform,
388
+ dismissible,
389
+ options,
390
+ optionId,
391
+ text,
392
+ questions,
393
+ answers,
394
+ ...additionalApprovalFields
395
+ } = response
396
+ ? {
397
+ ...approval,
398
+ approved: response.approved,
399
+ ...(response.reason != null && { reason: response.reason }),
400
+ ...(response.optionId != null && { optionId: response.optionId }),
401
+ ...(response.text != null && { text: response.text }),
402
+ ...(response.answers != null && { answers: response.answers }),
403
+ }
404
+ : approval;
405
+ const normalizedOptions = supportsRichToolApprovalResponses
406
+ ? normalizeToolApprovalOptions(options)
407
+ : undefined;
408
+ const normalizedQuestions = supportsRichToolApprovalResponses
409
+ ? normalizeToolApprovalQuestions(questions)
410
+ : undefined;
411
+ const normalizedAnswers = supportsRichToolApprovalResponses
412
+ ? normalizeToolApprovalAnswers(answers)
413
+ : undefined;
414
+ const requestReason = additionalApprovalFields.requestReason;
415
+ if (typeof id === "string")
416
+ return {
417
+ approval: {
418
+ ...additionalApprovalFields,
419
+ id,
420
+ ...(typeof prompt === "string"
421
+ ? { prompt }
422
+ : typeof requestReason === "string"
423
+ ? { prompt: requestReason }
424
+ : {}),
425
+ ...(typeof approved === "boolean" && { approved }),
426
+ ...(typeof reason === "string" && { reason }),
427
+ ...(isAutomatic === true && { isAutomatic: true }),
428
+ ...(supportsRichToolApprovalResponses && {
429
+ ...((display === "decision" ||
430
+ display === "select" ||
431
+ display === "text" ||
432
+ display === "questions") && { display }),
433
+ ...(typeof allowFreeform === "boolean" && { allowFreeform }),
434
+ ...(typeof dismissible === "boolean" && { dismissible }),
435
+ ...(normalizedOptions && { options: normalizedOptions }),
436
+ ...(typeof optionId === "string" && { optionId }),
437
+ ...(typeof text === "string" && { text }),
438
+ ...(normalizedQuestions && { questions: normalizedQuestions }),
439
+ ...(normalizedAnswers && { answers: normalizedAnswers }),
440
+ }),
441
+ ...((resolution === "cancelled" || resolution === "expired") && {
442
+ resolution,
443
+ }),
444
+ } as NonNullable<ToolCallMessagePart["approval"]>,
445
+ };
446
+ }
447
+
448
+ if (toolStatus?.type === "interrupt") {
449
+ return {
450
+ interrupt: toolStatus.payload as NonNullable<
451
+ ToolCallMessagePart["interrupt"]
452
+ >,
453
+ };
454
+ }
455
+
456
+ return {};
457
+ }
458
+
459
+ type MessageContent = Exclude<ThreadMessageLike["content"], string>;
460
+
461
+ const uiPartStateToStatus = (
462
+ state: "streaming" | "done" | undefined,
463
+ ): MessagePartStreamStatus | undefined => {
464
+ if (state === "streaming") return { type: "running" };
465
+ if (state === "done") return { type: "complete" };
466
+ return undefined;
467
+ };
468
+
469
+ const toSystemContent = (content: MessageContent): MessageContent => {
470
+ const text = content.filter((part) => part.type === "text");
471
+ if (text.length === 1) return text;
472
+ const providerMetadata = text.reduce<PartProviderMetadata>(
473
+ (merged, part) =>
474
+ part.providerMetadata != null
475
+ ? { ...merged, ...part.providerMetadata }
476
+ : merged,
477
+ {},
478
+ );
479
+ return [
480
+ {
481
+ type: "text",
482
+ text: text.map((part) => part.text).join(""),
483
+ ...(Object.keys(providerMetadata).length > 0 && { providerMetadata }),
484
+ },
485
+ ];
486
+ };
487
+
488
+ function convertParts(
489
+ message: UIMessage,
490
+ metadata: AISDKMessageConverterMetadata,
491
+ ): MessageContent {
492
+ if (!message.parts || message.parts.length === 0) {
493
+ return [];
494
+ }
495
+
496
+ const converted = message.parts
497
+ .filter(
498
+ (p) =>
499
+ typeof p?.type === "string" &&
500
+ p.type !== "step-start" &&
501
+ (message.role !== "user" || p.type !== "file"),
502
+ )
503
+ .map((part) => {
504
+ if (part.type === "text") {
505
+ const status = uiPartStateToStatus(part.state);
506
+ return {
507
+ type: "text",
508
+ text: part.text ?? "",
509
+ ...(status != null ? { status } : undefined),
510
+ ...(part.providerMetadata != null
511
+ ? {
512
+ providerMetadata: part.providerMetadata as PartProviderMetadata,
513
+ }
514
+ : undefined),
515
+ } satisfies TextMessagePart;
516
+ }
517
+
518
+ if (part.type === "reasoning") {
519
+ const status = uiPartStateToStatus(part.state);
520
+ return {
521
+ type: "reasoning",
522
+ text: part.text,
523
+ ...(status != null ? { status } : undefined),
524
+ ...(part.providerMetadata != null
525
+ ? {
526
+ providerMetadata: part.providerMetadata as PartProviderMetadata,
527
+ }
528
+ : undefined),
529
+ } satisfies ReasoningMessagePart;
530
+ }
531
+
532
+ if (isToolUIPart(part)) {
533
+ const toolName = getToolName(part) ?? "";
534
+ const toolCallId = part.toolCallId;
535
+ const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;
536
+
537
+ // A tool call that streamed complete arguments then failed schema
538
+ // validation keeps them in `rawInput`, not `input`; reading `input`
539
+ // alone would convert the error snapshot to `{}` and hide the input.
540
+ const rawInput = (part.input ??
541
+ ("rawInput" in part ? part.rawInput : undefined)) as
542
+ | ReadonlyJSONObject
543
+ | null
544
+ | undefined;
545
+ let args: ReadonlyJSONObject;
546
+ if (
547
+ rawInput != null &&
548
+ typeof rawInput === "object" &&
549
+ !Array.isArray(rawInput)
550
+ ) {
551
+ args = rawInput;
552
+ metadata.toolLastInputCache?.set(argsKeyOrderCacheKey, args);
553
+ } else {
554
+ args = metadata.toolLastInputCache?.get(argsKeyOrderCacheKey) ?? {};
555
+ }
556
+
557
+ let result: unknown;
558
+ let modelContent: ToolCallMessagePart["modelContent"];
559
+ let isError = false;
560
+
561
+ if (part.state === "output-available") {
562
+ const unwrapped = unwrapModelContentEnvelope(part.output);
563
+ result = unwrapped.result;
564
+ modelContent = unwrapped.modelContent;
565
+ } else if (part.state === "output-error") {
566
+ isError = true;
567
+ result = { error: part.errorText };
568
+ } else if (part.state === "output-denied") {
569
+ isError = true;
570
+ result = {
571
+ error:
572
+ (part as { approval?: { reason?: string } }).approval?.reason ||
573
+ "Tool approval denied",
574
+ };
575
+ }
576
+
577
+ let argsText: string;
578
+ if (part.state === "input-streaming") {
579
+ argsText = stableStringifyToolArgs(
580
+ metadata.toolArgsKeyOrderCache,
581
+ argsKeyOrderCacheKey,
582
+ args,
583
+ );
584
+ // strip closing delimiters added by the AI SDK's fix-json
585
+ argsText = stripClosingDelimiters(argsText);
586
+ // Re-parse so args carries the partial-JSON meta that marks which
587
+ // field is still mid-arrival, like every argsText-based runtime.
588
+ // The key-order cache appends new keys last, so the trailing field
589
+ // of the stripped text is the streaming frontier.
590
+ args = parsePartialJsonObject(argsText) ?? args;
591
+ } else {
592
+ // A settled part is re-converted whenever its message or the converter
593
+ // metadata changes; the text frozen on its input object skips
594
+ // re-serializing large args while the call keeps that input. Arrival
595
+ // order only matters while args stream, so the key-order entry is
596
+ // released.
597
+ const inputArgs = args;
598
+ const frozen =
599
+ metadata.toolArgsTextCache?.get(inputArgs) ??
600
+ new Map<string, { argsText: string; args: ReadonlyJSONObject }>();
601
+ const frozenEntry = frozen.get(argsKeyOrderCacheKey);
602
+ if (frozenEntry !== undefined) {
603
+ argsText = frozenEntry.argsText;
604
+ args = frozenEntry.args;
605
+ } else {
606
+ argsText = stableStringifyToolArgs(
607
+ metadata.toolArgsKeyOrderCache,
608
+ argsKeyOrderCacheKey,
609
+ args,
610
+ );
611
+ // The input is final even while execution keeps the part running.
612
+ // Other runtimes can synthesize complete JSON text from an
613
+ // accumulating snapshot, so only this converter supplies the
614
+ // completion signal it knows from the AI SDK part state.
615
+ // A complete root marker settles every field without parsing the
616
+ // serialized text again. Keep the SDK input's nested identities
617
+ // and own fields, including prototype-named JSON keys.
618
+ args = markPartialJsonObjectComplete(args);
619
+ metadata.toolArgsTextCache?.set(
620
+ inputArgs,
621
+ frozen.set(argsKeyOrderCacheKey, { argsText, args }),
622
+ );
623
+ }
624
+ metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);
625
+ if (
626
+ part.state === "output-available" ||
627
+ part.state === "output-error" ||
628
+ part.state === "output-denied"
629
+ ) {
630
+ metadata.toolLastInputCache?.delete(argsKeyOrderCacheKey);
631
+ }
632
+ }
633
+
634
+ const toolStatus = metadata.toolStatuses?.[toolCallId];
635
+ const mcpApp = extractMcpAppMetadata(
636
+ part,
637
+ metadata.mcpAppMetadataCache,
638
+ );
639
+ const artifact = metadata.toolArtifacts?.get(toolCallId);
640
+ const interactions = metadata.toolInteractions?.get(toolCallId);
641
+ return {
642
+ type: "tool-call",
643
+ toolName,
644
+ toolCallId,
645
+ argsText,
646
+ args,
647
+ result,
648
+ isError,
649
+ ...(artifact !== undefined && { artifact }),
650
+ ...(interactions !== undefined && {
651
+ unstable_interactions: interactions,
652
+ }),
653
+ ...(part.state === "output-available" &&
654
+ part.preliminary === true && { isPreliminary: true }),
655
+ ...(modelContent !== undefined && { modelContent }),
656
+ ...(mcpApp && { mcp: { app: mcpApp } }),
657
+ ...(part.callProviderMetadata != null
658
+ ? {
659
+ providerMetadata:
660
+ part.callProviderMetadata as PartProviderMetadata,
661
+ }
662
+ : undefined),
663
+ ...getToolApprovalAndInterrupt(
664
+ part,
665
+ toolStatus,
666
+ metadata.supportsRichToolApprovalResponses === true,
667
+ metadata.toolApprovalResponses,
668
+ metadata.cancelledToolApprovalIds,
669
+ ),
670
+ } satisfies ToolCallMessagePart;
671
+ }
672
+
673
+ if (part.type === "source-url") {
674
+ return {
675
+ type: "source",
676
+ sourceType: "url",
677
+ id: part.sourceId,
678
+ url: part.url,
679
+ ...(part.title != null ? { title: part.title } : undefined),
680
+ ...(part.providerMetadata != null
681
+ ? {
682
+ providerMetadata:
683
+ part.providerMetadata as SourceProviderMetadata,
684
+ }
685
+ : undefined),
686
+ } satisfies SourceMessagePart;
687
+ }
688
+
689
+ if (part.type === "file") {
690
+ if (typeof part.url !== "string") return null;
691
+ return {
692
+ type: "file",
693
+ data: part.url,
694
+ mimeType: part.mediaType ?? "unknown/unknown",
695
+ ...(part.filename != null && { filename: part.filename }),
696
+ ...(part.providerMetadata != null && {
697
+ providerMetadata: part.providerMetadata as PartProviderMetadata,
698
+ }),
699
+ } satisfies FileMessagePart;
700
+ }
701
+
702
+ if (part.type === "source-document") {
703
+ return {
704
+ type: "source",
705
+ sourceType: "document",
706
+ id: part.sourceId,
707
+ title: part.title,
708
+ mediaType: part.mediaType,
709
+ ...(part.filename != null ? { filename: part.filename } : undefined),
710
+ ...(part.providerMetadata != null
711
+ ? {
712
+ providerMetadata:
713
+ part.providerMetadata as SourceProviderMetadata,
714
+ }
715
+ : undefined),
716
+ } satisfies SourceMessagePart;
717
+ }
718
+
719
+ if (part.type.startsWith("data-")) {
720
+ return {
721
+ type: "data",
722
+ ...("id" in part && typeof part.id === "string"
723
+ ? { id: part.id }
724
+ : undefined),
725
+ name: part.type.substring(5),
726
+ data: (part as any).data,
727
+ } satisfies DataMessagePart;
728
+ }
729
+
730
+ if (isReasoningFileUIPart(part)) {
731
+ if (typeof part.url !== "string") return null;
732
+ return {
733
+ type: "file",
734
+ data: part.url,
735
+ mimeType: part.mediaType ?? "unknown/unknown",
736
+ ...(part.providerMetadata != null && {
737
+ providerMetadata: part.providerMetadata as PartProviderMetadata,
738
+ }),
739
+ } satisfies FileMessagePart;
740
+ }
741
+
742
+ if (isCustomContentUIPart(part)) {
743
+ return {
744
+ type: "data",
745
+ name: part.kind,
746
+ data: part.providerMetadata ?? null,
747
+ } satisfies DataMessagePart;
748
+ }
749
+
750
+ console.warn(`Unsupported message part type: ${part.type}`);
751
+ return null;
752
+ })
753
+ .filter(Boolean) as MessageContent[number][];
754
+
755
+ const seenToolCallIds = new Set<string>();
756
+ return converted.filter((part) => {
757
+ if (part.type === "tool-call" && part.toolCallId != null) {
758
+ if (seenToolCallIds.has(part.toolCallId)) return false;
759
+ seenToolCallIds.add(part.toolCallId);
760
+ }
761
+ return true;
762
+ });
763
+ }
764
+
765
+ export const convertAISDKMessage: useExternalMessageConverter.Callback<
766
+ UIMessage
767
+ > = (message, metadata: AISDKMessageConverterMetadata) => {
768
+ const createdAt = metadata.createdAtCache?.get(message.id) ?? new Date();
769
+ metadata.createdAtCache?.set(message.id, createdAt);
770
+ const content = convertParts(message, metadata);
771
+
772
+ switch (message.role) {
773
+ case "user":
774
+ return {
775
+ role: "user",
776
+ id: message.id,
777
+ createdAt,
778
+ content,
779
+ attachments: message.parts
780
+ ?.filter(
781
+ (p): p is FileUIPart =>
782
+ p?.type === "file" && typeof p.url === "string",
783
+ )
784
+ .map((part, idx) => {
785
+ const mediaType = part.mediaType ?? "unknown/unknown";
786
+ const isImage = mediaType.startsWith("image/");
787
+ return {
788
+ id: idx.toString(),
789
+ type: isImage ? "image" : "file",
790
+ name: part.filename ?? "file",
791
+ content: [
792
+ isImage
793
+ ? {
794
+ type: "image",
795
+ image: part.url,
796
+ ...(part.providerMetadata != null && {
797
+ providerMetadata:
798
+ part.providerMetadata as PartProviderMetadata,
799
+ }),
800
+ filename: part.filename!,
801
+ }
802
+ : {
803
+ type: "file",
804
+ filename: part.filename!,
805
+ data: part.url,
806
+ mimeType: mediaType,
807
+ ...(part.providerMetadata != null && {
808
+ providerMetadata:
809
+ part.providerMetadata as PartProviderMetadata,
810
+ }),
811
+ },
812
+ ],
813
+ contentType: mediaType,
814
+ status: { type: "complete" as const },
815
+ };
816
+ }),
817
+ metadata: toThreadMetadata(message.metadata),
818
+ };
819
+
820
+ case "system":
821
+ case "assistant": {
822
+ const timing = metadata.messageTiming?.[message.id];
823
+ const isOptimistic =
824
+ message.role === "assistant" &&
825
+ message.id === metadata.optimisticMessageId;
826
+ return {
827
+ role: message.role,
828
+ id: message.id,
829
+ createdAt,
830
+ content: message.role === "system" ? toSystemContent(content) : content,
831
+ ...(message.role === "assistant" &&
832
+ metadata.cancelledStatusMessageIds?.has(message.id)
833
+ ? {
834
+ status: {
835
+ type: "incomplete" as const,
836
+ reason: "cancelled" as const,
837
+ },
838
+ }
839
+ : undefined),
840
+ metadata: {
841
+ ...toThreadMetadata(message.metadata),
842
+ ...(timing && { timing }),
843
+ ...(isOptimistic && { isOptimistic: true }),
844
+ },
845
+ };
846
+ }
847
+
848
+ default:
849
+ console.warn(`Unsupported message role: ${message.role}`);
850
+ return [];
851
+ }
852
+ };
853
+
854
+ export const AISDKMessageConverter =
855
+ unstable_createMessageConverter(convertAISDKMessage);
856
+
857
+ const readMetadata = (metadata: useExternalMessageConverter.Metadata) =>
858
+ metadata as AISDKMessageConverterMetadata;
859
+
860
+ const getToolCallMetadata = (
861
+ message: UIMessage,
862
+ metadata: AISDKMessageConverterMetadata,
863
+ ): readonly unknown[] => {
864
+ const values: unknown[] = [];
865
+ for (const part of message.parts ?? []) {
866
+ if (typeof part?.type !== "string" || !isToolUIPart(part)) continue;
867
+ const { toolCallId } = part;
868
+ const approval = (part as { approval?: { id?: unknown } }).approval;
869
+ const approvalId = approval?.id;
870
+ values.push(
871
+ metadata.toolStatuses?.[toolCallId],
872
+ metadata.toolArtifacts?.get(toolCallId),
873
+ metadata.toolInteractions?.get(toolCallId),
874
+ approval ? metadata.supportsRichToolApprovalResponses === true : false,
875
+ typeof approvalId === "string"
876
+ ? metadata.toolApprovalResponses?.get(approvalId)
877
+ : undefined,
878
+ typeof approvalId === "string" &&
879
+ metadata.cancelledToolApprovalIds?.has(approvalId) === true,
880
+ );
881
+ }
882
+ return values;
883
+ };
884
+
885
+ // Every metadata read in convertAISDKMessage needs a selector here, or a
886
+ // change to it leaves the message it affects rendering stale output. The
887
+ // metadata-key test has a case per metadata field, so a new field fails
888
+ // typecheck until its invalidation is pinned.
889
+ export const createAISDKMetadataKey =
890
+ (): useExternalMessageConverter.GetMetadataKey<UIMessage> =>
891
+ createExternalMessageMetadataKey<UIMessage>([
892
+ { select: (_, metadata) => readMetadata(metadata).createdAtCache },
893
+ { select: (_, metadata) => readMetadata(metadata).toolArgsKeyOrderCache },
894
+ { select: (_, metadata) => readMetadata(metadata).toolArgsTextCache },
895
+ { select: (_, metadata) => readMetadata(metadata).toolLastInputCache },
896
+ { select: (_, metadata) => readMetadata(metadata).mcpAppMetadataCache },
897
+ {
898
+ select: (message, metadata) =>
899
+ readMetadata(metadata).messageTiming?.[message.id],
900
+ },
901
+ {
902
+ select: (message, metadata) =>
903
+ message.id === readMetadata(metadata).optimisticMessageId,
904
+ },
905
+ {
906
+ select: (message, metadata) =>
907
+ readMetadata(metadata).cancelledStatusMessageIds?.has(message.id) ===
908
+ true,
909
+ },
910
+ {
911
+ select: (message, metadata) =>
912
+ getToolCallMetadata(message, readMetadata(metadata)),
913
+ isEqual: (previous, current) =>
914
+ shallowArrayEqual(
915
+ previous as readonly unknown[],
916
+ current as readonly unknown[],
917
+ ),
918
+ },
919
+ ]);