@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.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 (233) hide show
  1. package/README.md +32 -27
  2. package/dist/UIMessages.d.ts +46 -0
  3. package/dist/UIMessages.d.ts.map +1 -0
  4. package/dist/UIMessages.js +546 -0
  5. package/dist/UIMessages.js.map +1 -0
  6. package/dist/client/createTool.d.ts +129 -27
  7. package/dist/client/createTool.d.ts.map +1 -1
  8. package/dist/client/createTool.js +66 -12
  9. package/dist/client/createTool.js.map +1 -1
  10. package/dist/client/defaultComponent.d.ts +11 -0
  11. package/dist/client/defaultComponent.d.ts.map +1 -0
  12. package/dist/client/defaultComponent.js +7 -0
  13. package/dist/client/defaultComponent.js.map +1 -0
  14. package/dist/client/definePlaygroundAPI.d.ts +1323 -192
  15. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  16. package/dist/client/definePlaygroundAPI.js +52 -28
  17. package/dist/client/definePlaygroundAPI.js.map +1 -1
  18. package/dist/client/files.d.ts +20 -7
  19. package/dist/client/files.d.ts.map +1 -1
  20. package/dist/client/files.js +68 -11
  21. package/dist/client/files.js.map +1 -1
  22. package/dist/client/index.d.ts +1056 -965
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +242 -748
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/client/messages.d.ts +461 -0
  27. package/dist/client/messages.d.ts.map +1 -0
  28. package/dist/client/messages.js +106 -0
  29. package/dist/client/messages.js.map +1 -0
  30. package/dist/client/mockModel.d.ts +42 -0
  31. package/dist/client/mockModel.d.ts.map +1 -0
  32. package/dist/client/mockModel.js +175 -0
  33. package/dist/client/mockModel.js.map +1 -0
  34. package/dist/client/saveInputMessages.d.ts +20 -0
  35. package/dist/client/saveInputMessages.d.ts.map +1 -0
  36. package/dist/client/saveInputMessages.js +58 -0
  37. package/dist/client/saveInputMessages.js.map +1 -0
  38. package/dist/client/search.d.ts +346 -35
  39. package/dist/client/search.d.ts.map +1 -1
  40. package/dist/client/search.js +350 -39
  41. package/dist/client/search.js.map +1 -1
  42. package/dist/client/start.d.ts +84 -0
  43. package/dist/client/start.d.ts.map +1 -0
  44. package/dist/client/start.js +171 -0
  45. package/dist/client/start.js.map +1 -0
  46. package/dist/client/streamText.d.ts +46 -0
  47. package/dist/client/streamText.d.ts.map +1 -0
  48. package/dist/client/streamText.js +93 -0
  49. package/dist/client/streamText.js.map +1 -0
  50. package/dist/client/streaming.d.ts +3705 -32
  51. package/dist/client/streaming.d.ts.map +1 -1
  52. package/dist/client/streaming.js +141 -59
  53. package/dist/client/streaming.js.map +1 -1
  54. package/dist/client/threads.d.ts +46 -0
  55. package/dist/client/threads.d.ts.map +1 -0
  56. package/dist/client/threads.js +49 -0
  57. package/dist/client/threads.js.map +1 -0
  58. package/dist/client/types.d.ts +265 -128
  59. package/dist/client/types.d.ts.map +1 -1
  60. package/dist/client/utils.d.ts +4 -0
  61. package/dist/client/utils.d.ts.map +1 -0
  62. package/dist/client/utils.js +21 -0
  63. package/dist/client/utils.js.map +1 -0
  64. package/dist/component/_generated/api.d.ts +24 -2178
  65. package/dist/component/_generated/api.d.ts.map +1 -1
  66. package/dist/component/_generated/api.js +10 -1
  67. package/dist/component/_generated/api.js.map +1 -1
  68. package/dist/component/_generated/component.d.ts +3119 -0
  69. package/dist/component/_generated/component.d.ts.map +1 -0
  70. package/dist/component/_generated/component.js +11 -0
  71. package/dist/component/_generated/component.js.map +1 -0
  72. package/dist/component/_generated/dataModel.d.ts +4 -18
  73. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  74. package/dist/component/_generated/dataModel.js +11 -0
  75. package/dist/component/_generated/dataModel.js.map +1 -0
  76. package/dist/component/_generated/server.d.ts +10 -38
  77. package/dist/component/_generated/server.d.ts.map +1 -1
  78. package/dist/component/_generated/server.js +9 -5
  79. package/dist/component/_generated/server.js.map +1 -1
  80. package/dist/component/files.d.ts +16 -10
  81. package/dist/component/files.d.ts.map +1 -1
  82. package/dist/component/files.js +10 -2
  83. package/dist/component/files.js.map +1 -1
  84. package/dist/component/messages.d.ts +2553 -342
  85. package/dist/component/messages.d.ts.map +1 -1
  86. package/dist/component/messages.js +387 -154
  87. package/dist/component/messages.js.map +1 -1
  88. package/dist/component/schema.d.ts +5697 -3584
  89. package/dist/component/schema.d.ts.map +1 -1
  90. package/dist/component/schema.js +18 -41
  91. package/dist/component/schema.js.map +1 -1
  92. package/dist/component/streams.d.ts +35 -335
  93. package/dist/component/streams.d.ts.map +1 -1
  94. package/dist/component/streams.js +114 -73
  95. package/dist/component/streams.js.map +1 -1
  96. package/dist/component/threads.d.ts +16 -16
  97. package/dist/component/users.d.ts +4 -4
  98. package/dist/component/vector/index.d.ts +1 -1
  99. package/dist/component/vector/index.d.ts.map +1 -1
  100. package/dist/component/vector/index.js +1 -3
  101. package/dist/component/vector/index.js.map +1 -1
  102. package/dist/deltas.d.ts +43 -0
  103. package/dist/deltas.d.ts.map +1 -0
  104. package/dist/deltas.js +447 -0
  105. package/dist/deltas.js.map +1 -0
  106. package/dist/mapping.d.ts +20 -20
  107. package/dist/mapping.d.ts.map +1 -1
  108. package/dist/mapping.js +313 -96
  109. package/dist/mapping.js.map +1 -1
  110. package/dist/react/SmoothText.d.ts +5 -0
  111. package/dist/react/SmoothText.d.ts.map +1 -0
  112. package/dist/react/SmoothText.js +6 -0
  113. package/dist/react/SmoothText.js.map +1 -0
  114. package/dist/react/index.d.ts +5 -77
  115. package/dist/react/index.d.ts.map +1 -1
  116. package/dist/react/index.js +6 -160
  117. package/dist/react/index.js.map +1 -1
  118. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  119. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  120. package/dist/react/optimisticallySendMessage.js +35 -9
  121. package/dist/react/optimisticallySendMessage.js.map +1 -1
  122. package/dist/react/types.d.ts +4 -18
  123. package/dist/react/types.d.ts.map +1 -1
  124. package/dist/react/useDeltaStreams.d.ts +10 -0
  125. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  126. package/dist/react/useDeltaStreams.js +101 -0
  127. package/dist/react/useDeltaStreams.js.map +1 -0
  128. package/dist/react/useSmoothText.d.ts +13 -12
  129. package/dist/react/useSmoothText.d.ts.map +1 -1
  130. package/dist/react/useSmoothText.js +32 -15
  131. package/dist/react/useSmoothText.js.map +1 -1
  132. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  133. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  134. package/dist/react/useStreamingUIMessages.js +92 -0
  135. package/dist/react/useStreamingUIMessages.js.map +1 -0
  136. package/dist/react/useThreadMessages.d.ts +104 -0
  137. package/dist/react/useThreadMessages.d.ts.map +1 -0
  138. package/dist/react/useThreadMessages.js +148 -0
  139. package/dist/react/useThreadMessages.js.map +1 -0
  140. package/dist/react/useUIMessages.d.ts +96 -0
  141. package/dist/react/useUIMessages.d.ts.map +1 -0
  142. package/dist/react/useUIMessages.js +108 -0
  143. package/dist/react/useUIMessages.js.map +1 -0
  144. package/dist/shared.d.ts +20 -4
  145. package/dist/shared.d.ts.map +1 -1
  146. package/dist/shared.js +45 -8
  147. package/dist/shared.js.map +1 -1
  148. package/dist/validators.d.ts +22981 -5666
  149. package/dist/validators.d.ts.map +1 -1
  150. package/dist/validators.js +245 -137
  151. package/dist/validators.js.map +1 -1
  152. package/package.json +98 -50
  153. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  154. package/src/UIMessages.test.ts +273 -0
  155. package/src/UIMessages.ts +739 -0
  156. package/src/client/createTool.ts +293 -76
  157. package/src/client/defaultComponent.ts +17 -0
  158. package/src/client/definePlaygroundAPI.ts +67 -31
  159. package/src/client/files.ts +100 -20
  160. package/src/client/index.test.ts +40 -85
  161. package/src/client/index.ts +520 -1290
  162. package/src/client/messages.ts +237 -0
  163. package/src/client/mockModel.ts +245 -0
  164. package/src/client/saveInputMessages.test.ts +583 -0
  165. package/src/client/saveInputMessages.ts +101 -0
  166. package/src/client/search.test.ts +1207 -0
  167. package/src/client/search.ts +577 -70
  168. package/src/client/start.ts +310 -0
  169. package/src/client/streamText.ts +163 -0
  170. package/src/client/streaming.test.ts +186 -0
  171. package/src/client/streaming.ts +219 -97
  172. package/src/client/threads.ts +83 -0
  173. package/src/client/types.ts +368 -219
  174. package/src/client/utils.ts +27 -0
  175. package/src/component/_generated/api.ts +64 -0
  176. package/src/component/_generated/component.ts +4913 -0
  177. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  178. package/src/component/files.ts +11 -2
  179. package/src/component/messages.test.ts +195 -51
  180. package/src/component/messages.ts +490 -201
  181. package/src/component/schema.ts +20 -46
  182. package/src/component/setup.test.ts +7 -0
  183. package/src/component/streams.ts +184 -83
  184. package/src/component/users.test.ts +0 -1
  185. package/src/component/vector/index.ts +1 -3
  186. package/src/deltas.test.ts +626 -0
  187. package/src/deltas.ts +570 -0
  188. package/src/fromUIMessages.test.ts +497 -0
  189. package/src/mapping.test.ts +103 -6
  190. package/src/mapping.ts +422 -161
  191. package/src/react/SmoothText.tsx +9 -0
  192. package/src/react/index.ts +10 -230
  193. package/src/react/optimisticallySendMessage.ts +55 -12
  194. package/src/react/types.ts +6 -39
  195. package/src/react/useDeltaStreams.ts +154 -0
  196. package/src/react/useSmoothText.ts +56 -36
  197. package/src/react/useStreamingUIMessages.ts +143 -0
  198. package/src/react/useThreadMessages.ts +262 -0
  199. package/src/react/useUIMessages.test.ts +255 -0
  200. package/src/react/useUIMessages.ts +195 -0
  201. package/src/shared.ts +88 -12
  202. package/src/test.ts +18 -0
  203. package/src/toUIMessages.test.ts +1269 -0
  204. package/src/validators.test.ts +18 -19
  205. package/src/validators.ts +325 -185
  206. package/dist/client/_generated/_ignore.d.ts +0 -1
  207. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  208. package/dist/client/_generated/_ignore.js +0 -3
  209. package/dist/client/_generated/_ignore.js.map +0 -1
  210. package/dist/client/listMessages.d.ts +0 -22
  211. package/dist/client/listMessages.d.ts.map +0 -1
  212. package/dist/client/listMessages.js +0 -25
  213. package/dist/client/listMessages.js.map +0 -1
  214. package/dist/package.json +0 -3
  215. package/dist/react/deltas.d.ts +0 -26
  216. package/dist/react/deltas.d.ts.map +0 -1
  217. package/dist/react/deltas.js +0 -384
  218. package/dist/react/deltas.js.map +0 -1
  219. package/dist/react/toUIMessages.d.ts +0 -15
  220. package/dist/react/toUIMessages.d.ts.map +0 -1
  221. package/dist/react/toUIMessages.js +0 -211
  222. package/dist/react/toUIMessages.js.map +0 -1
  223. package/src/client/listMessages.ts +0 -38
  224. package/src/component/_generated/api.d.ts +0 -2202
  225. package/src/component/_generated/api.js +0 -23
  226. package/src/component/_generated/server.js +0 -90
  227. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  228. package/src/react/deltas.test.ts +0 -315
  229. package/src/react/deltas.ts +0 -478
  230. package/src/react/toUIMessages.test.ts +0 -420
  231. package/src/react/toUIMessages.ts +0 -253
  232. package/src/vitest.config.ts +0 -7
  233. /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
@@ -0,0 +1,739 @@
1
+ import {
2
+ convertToModelMessages,
3
+ type UIMessage as AIUIMessage,
4
+ type DeepPartial,
5
+ type DynamicToolUIPart,
6
+ type ReasoningUIPart,
7
+ type SourceDocumentUIPart,
8
+ type SourceUrlUIPart,
9
+ type StepStartUIPart,
10
+ type TextUIPart,
11
+ type ToolResultPart,
12
+ type ToolUIPart,
13
+ type UIDataTypes,
14
+ type UITools,
15
+ } from "ai";
16
+ import type { Infer } from "convex/values";
17
+ import { toModelMessage, fromModelMessage, toUIFilePart } from "./mapping.js";
18
+ import {
19
+ extractReasoning,
20
+ extractText,
21
+ isTool,
22
+ joinText,
23
+ sorted,
24
+ } from "./shared.js";
25
+ import type {
26
+ MessageDoc,
27
+ MessageStatus,
28
+ ProviderOptions,
29
+ SourcePart,
30
+ vSource,
31
+ } from "./validators.js";
32
+ import { omit, pick } from "convex-helpers";
33
+
34
+ export type UIStatus = "streaming" | MessageStatus;
35
+
36
+ export type UIMessage<
37
+ METADATA = unknown,
38
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
39
+ TOOLS extends UITools = UITools,
40
+ > = AIUIMessage<METADATA, DATA_PARTS, TOOLS> & {
41
+ key: string;
42
+ order: number;
43
+ stepOrder: number;
44
+ status: UIStatus;
45
+ agentName?: string;
46
+ userId?: string;
47
+ text: string;
48
+ _creationTime: number;
49
+ };
50
+
51
+ /**
52
+ * Converts a list of UIMessages to MessageDocs, along with extra metadata that
53
+ * may be available to associate with the MessageDocs.
54
+ * @param messages - The UIMessages to convert to MessageDocs.
55
+ * @param meta - The metadata to add to the MessageDocs.
56
+ * @returns
57
+ */
58
+ export async function fromUIMessages<METADATA = unknown>(
59
+ messages: UIMessage<METADATA>[],
60
+ meta: {
61
+ threadId: string;
62
+ userId?: string;
63
+ model?: string;
64
+ provider?: string;
65
+ providerOptions?: ProviderOptions;
66
+ metadata?: METADATA;
67
+ },
68
+ ): Promise<(MessageDoc & { streaming: boolean; metadata?: METADATA })[]> {
69
+ const nested = await Promise.all(
70
+ messages.map(async (uiMessage) => {
71
+ const stepOrder = uiMessage.stepOrder;
72
+ const commonFields = {
73
+ ...pick(meta, [
74
+ "threadId",
75
+ "userId",
76
+ "model",
77
+ "provider",
78
+ "providerOptions",
79
+ "metadata",
80
+ ]),
81
+ ...omit(uiMessage, ["parts", "role", "key", "text", "userId"]),
82
+ userId: uiMessage.userId ?? meta.userId,
83
+ status: uiMessage.status === "streaming" ? "pending" : "success",
84
+ streaming: uiMessage.status === "streaming",
85
+ // to override
86
+ _id: uiMessage.id,
87
+ tool: false,
88
+ } satisfies MessageDoc & { streaming: boolean; metadata?: METADATA };
89
+ const modelMessages = await convertToModelMessages([uiMessage]);
90
+ return modelMessages
91
+ .map((modelMessage, i) => {
92
+ if (modelMessage.content.length === 0) {
93
+ return undefined;
94
+ }
95
+ const message = fromModelMessage(modelMessage);
96
+ const tool = isTool(message);
97
+ const doc: MessageDoc & { streaming: boolean; metadata?: METADATA } =
98
+ {
99
+ ...commonFields,
100
+ _id: uiMessage.id + `-${i}`,
101
+ stepOrder: stepOrder + i,
102
+ message,
103
+ tool,
104
+ text: extractText(message),
105
+ reasoning: extractReasoning(message),
106
+ finishReason: tool ? "tool-calls" : "stop",
107
+ sources: fromSourceParts(uiMessage.parts),
108
+ };
109
+ if (Array.isArray(modelMessage.content)) {
110
+ // Find a content part with providerOptions (type assertion needed for SDK compatibility)
111
+ const partWithProviderOptions = modelMessage.content.find(
112
+ (c): c is typeof c & { providerOptions: unknown } =>
113
+ "providerOptions" in c && c.providerOptions !== undefined,
114
+ );
115
+ if (partWithProviderOptions?.providerOptions) {
116
+ // convertToModelMessages changes providerMetadata to providerOptions
117
+ const providerOptions =
118
+ partWithProviderOptions.providerOptions as
119
+ | Record<string, Record<string, unknown>>
120
+ | undefined;
121
+ if (providerOptions) {
122
+ doc.providerMetadata = providerOptions;
123
+ doc.providerOptions ??= providerOptions;
124
+ }
125
+ }
126
+ }
127
+ return doc;
128
+ })
129
+ .filter((d) => d !== undefined);
130
+ }),
131
+ );
132
+ return nested.flat();
133
+ }
134
+
135
+ function fromSourceParts(parts: UIMessage["parts"]): Infer<typeof vSource>[] {
136
+ return parts
137
+ .map((part) => {
138
+ if (part.type === "source-url") {
139
+ return {
140
+ type: "source",
141
+ sourceType: "url",
142
+ url: part.url,
143
+ id: part.sourceId,
144
+ providerMetadata: part.providerMetadata,
145
+ title: part.title,
146
+ } satisfies Infer<typeof vSource>;
147
+ }
148
+ if (part.type === "source-document") {
149
+ return {
150
+ type: "source",
151
+ sourceType: "document",
152
+ mediaType: part.mediaType,
153
+ id: part.sourceId,
154
+ providerMetadata: part.providerMetadata,
155
+ title: part.title,
156
+ } satisfies Infer<typeof vSource>;
157
+ }
158
+ return undefined;
159
+ })
160
+ .filter((p) => p !== undefined);
161
+ }
162
+
163
+ type ExtraFields<METADATA = unknown> = {
164
+ streaming?: boolean;
165
+ metadata?: METADATA;
166
+ };
167
+
168
+ /**
169
+ * Converts a list of MessageDocs to UIMessages.
170
+ * This is somewhat lossy, as many fields are not supported by UIMessages, e.g.
171
+ * the model, provider, userId, etc.
172
+ * The UIMessage type is the augmented type that includes more fields such as
173
+ * key, order, stepOrder, status, agentName, text, etc.
174
+ */
175
+ export function toUIMessages<
176
+ METADATA = unknown,
177
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
178
+ TOOLS extends UITools = UITools,
179
+ >(
180
+ messages: (MessageDoc & ExtraFields<METADATA>)[],
181
+ ): UIMessage<METADATA, DATA_PARTS, TOOLS>[] {
182
+ // Group assistant and tool messages together
183
+ const assistantGroups = groupAssistantMessages(sorted(messages));
184
+
185
+ const uiMessages: UIMessage<METADATA, DATA_PARTS, TOOLS>[] = [];
186
+ for (const group of assistantGroups) {
187
+ if (group.role === "system") {
188
+ uiMessages.push(createSystemUIMessage(group.message));
189
+ } else if (group.role === "user") {
190
+ uiMessages.push(createUserUIMessage(group.message));
191
+ } else {
192
+ // Assistant/tool group
193
+ uiMessages.push(createAssistantUIMessage(group.messages));
194
+ }
195
+ }
196
+
197
+ return uiMessages;
198
+ }
199
+
200
+ type Group<METADATA = unknown> =
201
+ | {
202
+ role: "user";
203
+ message: MessageDoc & ExtraFields<METADATA>;
204
+ }
205
+ | {
206
+ role: "system";
207
+ message: MessageDoc & ExtraFields<METADATA>;
208
+ }
209
+ | {
210
+ role: "assistant";
211
+ messages: (MessageDoc & ExtraFields<METADATA>)[];
212
+ };
213
+
214
+ function groupAssistantMessages<METADATA = unknown>(
215
+ messages: (MessageDoc & ExtraFields<METADATA>)[],
216
+ ): Group<METADATA>[] {
217
+ const groups: Group<METADATA>[] = [];
218
+
219
+ let currentAssistantGroup: (MessageDoc & ExtraFields<METADATA>)[] = [];
220
+ let currentOrder: number | undefined;
221
+
222
+ for (const message of messages) {
223
+ const coreMessage = message.message && toModelMessage(message.message);
224
+ if (!coreMessage) continue;
225
+
226
+ if (coreMessage.role === "user" || coreMessage.role === "system") {
227
+ // Finish any current assistant group
228
+ if (currentAssistantGroup.length > 0) {
229
+ groups.push({
230
+ role: "assistant",
231
+ messages: currentAssistantGroup,
232
+ });
233
+ currentAssistantGroup = [];
234
+ currentOrder = undefined;
235
+ }
236
+ // Add singleton group
237
+ groups.push({
238
+ role: coreMessage.role,
239
+ message,
240
+ });
241
+ } else {
242
+ // Assistant or tool message
243
+
244
+ // Start new group if order changes or this is the first assistant/tool message
245
+ if (currentOrder !== undefined && message.order !== currentOrder) {
246
+ if (currentAssistantGroup.length > 0) {
247
+ groups.push({
248
+ role: "assistant",
249
+ messages: currentAssistantGroup,
250
+ });
251
+ currentAssistantGroup = [];
252
+ }
253
+ }
254
+
255
+ currentOrder = message.order;
256
+ currentAssistantGroup.push(message);
257
+
258
+ // End group if this is an assistant message without tool calls
259
+ if (coreMessage.role === "assistant" && !message.tool) {
260
+ groups.push({
261
+ role: "assistant",
262
+ messages: currentAssistantGroup,
263
+ });
264
+ currentAssistantGroup = [];
265
+ currentOrder = undefined;
266
+ }
267
+ }
268
+ }
269
+
270
+ // Add any remaining assistant group
271
+ if (currentAssistantGroup.length > 0) {
272
+ groups.push({
273
+ role: "assistant",
274
+ messages: currentAssistantGroup,
275
+ });
276
+ }
277
+
278
+ return groups;
279
+ }
280
+
281
+ function createSystemUIMessage<
282
+ METADATA = unknown,
283
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
284
+ TOOLS extends UITools = UITools,
285
+ >(
286
+ message: MessageDoc & ExtraFields<METADATA>,
287
+ ): UIMessage<METADATA, DATA_PARTS, TOOLS> {
288
+ const text = extractTextFromMessageDoc(message);
289
+ const partCommon = {
290
+ state: message.streaming ? ("streaming" as const) : ("done" as const),
291
+ ...(message.providerMetadata
292
+ ? { providerMetadata: message.providerMetadata }
293
+ : {}),
294
+ };
295
+
296
+ return {
297
+ id: message._id,
298
+ _creationTime: message._creationTime,
299
+ order: message.order,
300
+ stepOrder: message.stepOrder,
301
+ status: message.streaming ? ("streaming" as const) : message.status,
302
+ key: `${message.threadId}-${message.order}-${message.stepOrder}`,
303
+ text,
304
+ role: "system",
305
+ agentName: message.agentName,
306
+ userId: message.userId,
307
+ parts: [{ type: "text", text, ...partCommon } satisfies TextUIPart],
308
+ metadata: message.metadata,
309
+ };
310
+ }
311
+
312
+ function extractTextFromMessageDoc(message: MessageDoc): string {
313
+ return (
314
+ (message.message && extractText(message.message)) || message.text || ""
315
+ );
316
+ }
317
+
318
+ function createUserUIMessage<
319
+ METADATA = unknown,
320
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
321
+ TOOLS extends UITools = UITools,
322
+ >(
323
+ message: MessageDoc & ExtraFields<METADATA>,
324
+ ): UIMessage<METADATA, DATA_PARTS, TOOLS> {
325
+ const text = extractTextFromMessageDoc(message);
326
+ const coreMessage = toModelMessage(message.message!);
327
+ const content = coreMessage.content;
328
+ const nonStringContent =
329
+ content && typeof content !== "string" ? content : [];
330
+
331
+ const partCommon = {
332
+ state: message.streaming ? ("streaming" as const) : ("done" as const),
333
+ ...(message.providerMetadata
334
+ ? { providerMetadata: message.providerMetadata }
335
+ : {}),
336
+ };
337
+
338
+ const parts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
339
+ if (text && !nonStringContent.length) {
340
+ parts.push({ type: "text", text });
341
+ }
342
+ for (const contentPart of nonStringContent) {
343
+ switch (contentPart.type) {
344
+ case "text":
345
+ parts.push({ type: "text", text: contentPart.text, ...partCommon });
346
+ break;
347
+ case "file":
348
+ case "image":
349
+ parts.push(toUIFilePart(contentPart));
350
+ break;
351
+ default:
352
+ console.warn("Unknown content part type for user", contentPart);
353
+ break;
354
+ }
355
+ }
356
+
357
+ return {
358
+ id: message._id,
359
+ _creationTime: message._creationTime,
360
+ order: message.order,
361
+ stepOrder: message.stepOrder,
362
+ status: message.streaming ? ("streaming" as const) : message.status,
363
+ key: `${message.threadId}-${message.order}-${message.stepOrder}`,
364
+ text,
365
+ role: "user",
366
+ userId: message.userId,
367
+ parts,
368
+ metadata: message.metadata,
369
+ };
370
+ }
371
+
372
+ function createAssistantUIMessage<
373
+ METADATA = unknown,
374
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
375
+ TOOLS extends UITools = UITools,
376
+ >(
377
+ groupUnordered: (MessageDoc & ExtraFields<METADATA>)[],
378
+ ): UIMessage<METADATA, DATA_PARTS, TOOLS> {
379
+ const group = sorted(groupUnordered);
380
+ const firstMessage = group[0];
381
+
382
+ // Use first message for special fields
383
+ const common = {
384
+ id: firstMessage._id,
385
+ _creationTime: firstMessage._creationTime,
386
+ order: firstMessage.order,
387
+ stepOrder: firstMessage.stepOrder,
388
+ key: `${firstMessage.threadId}-${firstMessage.order}-${firstMessage.stepOrder}`,
389
+ agentName: firstMessage.agentName,
390
+ userId: firstMessage.userId,
391
+ };
392
+
393
+ // Get status from last message
394
+ const lastMessage = group[group.length - 1];
395
+ const status = lastMessage.streaming
396
+ ? ("streaming" as const)
397
+ : lastMessage.status;
398
+
399
+ // Collect all parts from all messages
400
+ const allParts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
401
+
402
+ for (const message of group) {
403
+ const coreMessage = message.message && toModelMessage(message.message);
404
+ if (!coreMessage) continue;
405
+
406
+ const content = coreMessage.content;
407
+ const nonStringContent =
408
+ content && typeof content !== "string" ? content : [];
409
+ const text = extractTextFromMessageDoc(message);
410
+
411
+ const partCommon = {
412
+ state: message.streaming ? ("streaming" as const) : ("done" as const),
413
+ ...(message.providerMetadata
414
+ ? { providerMetadata: message.providerMetadata }
415
+ : {}),
416
+ };
417
+
418
+ // Add reasoning parts
419
+ if (
420
+ message.reasoning &&
421
+ !nonStringContent.some((c) => c.type === "reasoning")
422
+ ) {
423
+ allParts.push({
424
+ type: "reasoning",
425
+ text: message.reasoning,
426
+ ...partCommon,
427
+ } satisfies ReasoningUIPart);
428
+ }
429
+
430
+ // Add text parts if no structured content
431
+ if (text && !nonStringContent.length) {
432
+ allParts.push({
433
+ type: "text",
434
+ text: text,
435
+ ...partCommon,
436
+ } satisfies TextUIPart);
437
+ }
438
+
439
+ // Add all structured content parts
440
+ for (const contentPart of nonStringContent) {
441
+ switch (contentPart.type) {
442
+ case "text":
443
+ allParts.push({
444
+ ...partCommon,
445
+ ...contentPart,
446
+ } satisfies TextUIPart);
447
+ break;
448
+ case "reasoning":
449
+ allParts.push({
450
+ ...partCommon,
451
+ ...contentPart,
452
+ } satisfies ReasoningUIPart);
453
+ break;
454
+ case "file":
455
+ case "image":
456
+ allParts.push(toUIFilePart(contentPart));
457
+ break;
458
+ case "tool-call": {
459
+ allParts.push({
460
+ type: "step-start",
461
+ } satisfies StepStartUIPart);
462
+ const toolPart: ToolUIPart<TOOLS> = {
463
+ type: `tool-${contentPart.toolName as keyof TOOLS & string}`,
464
+ toolCallId: contentPart.toolCallId,
465
+ input: contentPart.input as DeepPartial<
466
+ TOOLS[keyof TOOLS & string]["input"]
467
+ >,
468
+ providerExecuted: contentPart.providerExecuted,
469
+ ...(message.streaming
470
+ ? { state: "input-streaming" }
471
+ : {
472
+ state: "input-available",
473
+ callProviderMetadata: message.providerMetadata,
474
+ }),
475
+ };
476
+ allParts.push(toolPart);
477
+ break;
478
+ }
479
+ case "tool-result": {
480
+ const typedPart = contentPart as unknown as ToolResultPart & {
481
+ output: { type: string; value?: unknown; reason?: string };
482
+ };
483
+
484
+ // Check if this is an execution-denied result
485
+ if (typedPart.output?.type === "execution-denied") {
486
+ const call = allParts.find(
487
+ (part) =>
488
+ part.type === `tool-${contentPart.toolName}` &&
489
+ "toolCallId" in part &&
490
+ part.toolCallId === contentPart.toolCallId,
491
+ ) as ToolUIPart | undefined;
492
+
493
+ if (call) {
494
+ call.state = "output-denied";
495
+ if (!("approval" in call) || !call.approval) {
496
+ (call as ToolUIPart & { approval?: object }).approval = {
497
+ id: "",
498
+ approved: false,
499
+ reason: typedPart.output.reason,
500
+ };
501
+ } else {
502
+ const approval = (
503
+ call as ToolUIPart & {
504
+ approval: { approved?: boolean; reason?: string };
505
+ }
506
+ ).approval;
507
+ approval.approved = false;
508
+ approval.reason = typedPart.output.reason;
509
+ }
510
+ }
511
+ break;
512
+ }
513
+
514
+ const output =
515
+ typeof typedPart.output?.type === "string"
516
+ ? typedPart.output.value
517
+ : typedPart.output;
518
+ // Check for error at both the content part level (isError) and message level
519
+ // isError may exist on stored tool results but isn't in ToolResultPart type
520
+ const hasError =
521
+ (contentPart as { isError?: boolean }).isError || message.error;
522
+ const errorText =
523
+ message.error || (hasError ? String(output) : undefined);
524
+ const call = allParts.find(
525
+ (part) =>
526
+ part.type === `tool-${contentPart.toolName}` &&
527
+ "toolCallId" in part &&
528
+ part.toolCallId === contentPart.toolCallId,
529
+ ) as ToolUIPart | undefined;
530
+ if (call) {
531
+ if (hasError) {
532
+ call.state = "output-error";
533
+ call.errorText = errorText ?? "Unknown error";
534
+ call.output = output;
535
+ } else {
536
+ call.state = "output-available";
537
+ call.output = output;
538
+ }
539
+ } else {
540
+ // Tool call is on a previous page - create standalone tool part
541
+ if (hasError) {
542
+ allParts.push({
543
+ type: `tool-${contentPart.toolName}`,
544
+ toolCallId: contentPart.toolCallId,
545
+ state: "output-error",
546
+ input: undefined,
547
+ errorText: errorText ?? "Unknown error",
548
+ callProviderMetadata: message.providerMetadata,
549
+ } satisfies ToolUIPart<TOOLS>);
550
+ } else {
551
+ allParts.push({
552
+ type: `tool-${contentPart.toolName}`,
553
+ toolCallId: contentPart.toolCallId,
554
+ state: "output-available",
555
+ input: undefined,
556
+ output,
557
+ callProviderMetadata: message.providerMetadata,
558
+ } satisfies ToolUIPart<TOOLS>);
559
+ }
560
+ }
561
+ break;
562
+ }
563
+ case "tool-approval-request": {
564
+ // Find the matching tool call
565
+ const typedPart = contentPart as {
566
+ toolCallId: string;
567
+ approvalId: string;
568
+ };
569
+ const toolCallPart = allParts.find(
570
+ (part) =>
571
+ "toolCallId" in part && part.toolCallId === typedPart.toolCallId,
572
+ ) as ToolUIPart | undefined;
573
+
574
+ if (toolCallPart) {
575
+ toolCallPart.state = "approval-requested";
576
+ (toolCallPart as ToolUIPart & { approval?: object }).approval = {
577
+ id: typedPart.approvalId,
578
+ };
579
+ } else {
580
+ console.warn(
581
+ "Tool approval request without preceding tool call",
582
+ contentPart,
583
+ );
584
+ }
585
+ break;
586
+ }
587
+ case "tool-approval-response": {
588
+ // Find the tool call that has this approval by matching approval.id
589
+ const typedPart = contentPart as {
590
+ approvalId: string;
591
+ approved: boolean;
592
+ reason?: string;
593
+ };
594
+ const toolCallPart = allParts.find(
595
+ (part) =>
596
+ "approval" in part &&
597
+ (part as ToolUIPart & { approval?: { id: string } }).approval
598
+ ?.id === typedPart.approvalId,
599
+ ) as ToolUIPart | undefined;
600
+
601
+ if (toolCallPart) {
602
+ if (typedPart.approved) {
603
+ toolCallPart.state = "approval-responded";
604
+ (toolCallPart as ToolUIPart & { approval?: object }).approval = {
605
+ id: typedPart.approvalId,
606
+ approved: true,
607
+ reason: typedPart.reason,
608
+ };
609
+ } else {
610
+ toolCallPart.state = "output-denied";
611
+ (toolCallPart as ToolUIPart & { approval?: object }).approval = {
612
+ id: typedPart.approvalId,
613
+ approved: false,
614
+ reason: typedPart.reason,
615
+ };
616
+ }
617
+ } else {
618
+ console.warn(
619
+ "Tool approval response without matching approval request",
620
+ contentPart,
621
+ );
622
+ }
623
+ break;
624
+ }
625
+ default: {
626
+ const maybeSource = contentPart as unknown as SourcePart;
627
+ if (maybeSource.type === "source") {
628
+ allParts.push(toSourcePart(maybeSource));
629
+ } else {
630
+ console.warn(
631
+ "Unknown content part type for assistant",
632
+ contentPart,
633
+ );
634
+ }
635
+ }
636
+ }
637
+ }
638
+
639
+ // Add source parts
640
+ for (const source of message.sources ?? []) {
641
+ allParts.push(toSourcePart(source));
642
+ }
643
+ }
644
+
645
+ return {
646
+ ...common,
647
+ role: "assistant",
648
+ text: joinText(allParts),
649
+ status,
650
+ parts: allParts,
651
+ metadata: group.find((m) => m.metadata)?.metadata,
652
+ };
653
+ }
654
+
655
+ function toSourcePart(
656
+ part: SourcePart | Infer<typeof vSource>,
657
+ ): SourceUrlUIPart | SourceDocumentUIPart {
658
+ if (part.sourceType === "url") {
659
+ return {
660
+ type: "source-url",
661
+ url: part.url,
662
+ sourceId: part.id,
663
+ providerMetadata: part.providerMetadata,
664
+ title: part.title,
665
+ } satisfies SourceUrlUIPart;
666
+ }
667
+ return {
668
+ type: "source-document",
669
+ mediaType: part.mediaType,
670
+ sourceId: part.id,
671
+ title: part.title,
672
+ filename: part.filename,
673
+ providerMetadata: part.providerMetadata,
674
+ } satisfies SourceDocumentUIPart;
675
+ }
676
+
677
+ export function combineUIMessages(messages: UIMessage[]): UIMessage[] {
678
+ const combined = messages.reduce((acc, message) => {
679
+ if (!acc.length) {
680
+ return [message];
681
+ }
682
+ const previous = acc.at(-1)!;
683
+ if (
684
+ message.order !== previous.order ||
685
+ previous.role !== message.role ||
686
+ message.role !== "assistant"
687
+ ) {
688
+ acc.push(message);
689
+ return acc;
690
+ }
691
+ // We will replace it with a combined message
692
+ acc.pop();
693
+ const newParts = [...previous.parts];
694
+ for (const part of message.parts) {
695
+ const toolCallId = getToolCallId(part);
696
+ if (!toolCallId) {
697
+ newParts.push(part);
698
+ continue;
699
+ }
700
+ const previousPartIndex = newParts.findIndex(
701
+ (p) => getToolCallId(p) === toolCallId,
702
+ );
703
+ if (previousPartIndex === -1) {
704
+ // Tool call not found in previous parts, add it as new
705
+ newParts.push(part);
706
+ continue;
707
+ }
708
+ const previousPart = newParts.splice(previousPartIndex, 1)[0];
709
+ newParts.push(mergeParts(previousPart, part));
710
+ }
711
+ acc.push({
712
+ ...previous,
713
+ ...pick(message, ["status", "metadata", "agentName"]),
714
+ parts: newParts,
715
+ text: joinText(newParts),
716
+ });
717
+ return acc;
718
+ }, [] as UIMessage[]);
719
+ return combined;
720
+ }
721
+
722
+ function getToolCallId(
723
+ part: UIMessage["parts"][number] & { toolCallId?: string },
724
+ ) {
725
+ return part.toolCallId;
726
+ }
727
+
728
+ function mergeParts(
729
+ previousPart: UIMessage["parts"][number],
730
+ part: UIMessage["parts"][number],
731
+ ): UIMessage["parts"][number] {
732
+ const merged: Record<string, unknown> = { ...previousPart };
733
+ for (const [key, value] of Object.entries(part)) {
734
+ if (value !== undefined) {
735
+ merged[key] = value;
736
+ }
737
+ }
738
+ return merged as ToolUIPart | DynamicToolUIPart;
739
+ }