@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.1

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 (235) hide show
  1. package/MIGRATION.md +153 -0
  2. package/README.md +32 -27
  3. package/dist/UIMessages.d.ts +46 -0
  4. package/dist/UIMessages.d.ts.map +1 -0
  5. package/dist/UIMessages.js +546 -0
  6. package/dist/UIMessages.js.map +1 -0
  7. package/dist/client/createTool.d.ts +126 -27
  8. package/dist/client/createTool.d.ts.map +1 -1
  9. package/dist/client/createTool.js +67 -12
  10. package/dist/client/createTool.js.map +1 -1
  11. package/dist/client/defaultComponent.d.ts +11 -0
  12. package/dist/client/defaultComponent.d.ts.map +1 -0
  13. package/dist/client/defaultComponent.js +7 -0
  14. package/dist/client/defaultComponent.js.map +1 -0
  15. package/dist/client/definePlaygroundAPI.d.ts +1335 -204
  16. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  17. package/dist/client/definePlaygroundAPI.js +52 -28
  18. package/dist/client/definePlaygroundAPI.js.map +1 -1
  19. package/dist/client/files.d.ts +20 -7
  20. package/dist/client/files.d.ts.map +1 -1
  21. package/dist/client/files.js +68 -11
  22. package/dist/client/files.js.map +1 -1
  23. package/dist/client/index.d.ts +1116 -978
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +332 -747
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/messages.d.ts +461 -0
  28. package/dist/client/messages.d.ts.map +1 -0
  29. package/dist/client/messages.js +106 -0
  30. package/dist/client/messages.js.map +1 -0
  31. package/dist/client/mockModel.d.ts +42 -0
  32. package/dist/client/mockModel.d.ts.map +1 -0
  33. package/dist/client/mockModel.js +182 -0
  34. package/dist/client/mockModel.js.map +1 -0
  35. package/dist/client/saveInputMessages.d.ts +20 -0
  36. package/dist/client/saveInputMessages.d.ts.map +1 -0
  37. package/dist/client/saveInputMessages.js +58 -0
  38. package/dist/client/saveInputMessages.js.map +1 -0
  39. package/dist/client/search.d.ts +350 -39
  40. package/dist/client/search.d.ts.map +1 -1
  41. package/dist/client/search.js +350 -39
  42. package/dist/client/search.js.map +1 -1
  43. package/dist/client/start.d.ts +84 -0
  44. package/dist/client/start.d.ts.map +1 -0
  45. package/dist/client/start.js +185 -0
  46. package/dist/client/start.js.map +1 -0
  47. package/dist/client/streamText.d.ts +46 -0
  48. package/dist/client/streamText.d.ts.map +1 -0
  49. package/dist/client/streamText.js +117 -0
  50. package/dist/client/streamText.js.map +1 -0
  51. package/dist/client/streaming.d.ts +3716 -32
  52. package/dist/client/streaming.d.ts.map +1 -1
  53. package/dist/client/streaming.js +161 -59
  54. package/dist/client/streaming.js.map +1 -1
  55. package/dist/client/threads.d.ts +46 -0
  56. package/dist/client/threads.d.ts.map +1 -0
  57. package/dist/client/threads.js +49 -0
  58. package/dist/client/threads.js.map +1 -0
  59. package/dist/client/types.d.ts +266 -128
  60. package/dist/client/types.d.ts.map +1 -1
  61. package/dist/client/utils.d.ts +4 -0
  62. package/dist/client/utils.d.ts.map +1 -0
  63. package/dist/client/utils.js +21 -0
  64. package/dist/client/utils.js.map +1 -0
  65. package/dist/component/_generated/api.d.ts +24 -2178
  66. package/dist/component/_generated/api.d.ts.map +1 -1
  67. package/dist/component/_generated/api.js +10 -1
  68. package/dist/component/_generated/api.js.map +1 -1
  69. package/dist/component/_generated/component.d.ts +3120 -0
  70. package/dist/component/_generated/component.d.ts.map +1 -0
  71. package/dist/component/_generated/component.js +11 -0
  72. package/dist/component/_generated/component.js.map +1 -0
  73. package/dist/component/_generated/dataModel.d.ts +4 -18
  74. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  75. package/dist/component/_generated/dataModel.js +11 -0
  76. package/dist/component/_generated/dataModel.js.map +1 -0
  77. package/dist/component/_generated/server.d.ts +10 -38
  78. package/dist/component/_generated/server.d.ts.map +1 -1
  79. package/dist/component/_generated/server.js +9 -5
  80. package/dist/component/_generated/server.js.map +1 -1
  81. package/dist/component/files.d.ts +16 -10
  82. package/dist/component/files.d.ts.map +1 -1
  83. package/dist/component/files.js +10 -2
  84. package/dist/component/files.js.map +1 -1
  85. package/dist/component/messages.d.ts +2578 -366
  86. package/dist/component/messages.d.ts.map +1 -1
  87. package/dist/component/messages.js +397 -154
  88. package/dist/component/messages.js.map +1 -1
  89. package/dist/component/schema.d.ts +5697 -3584
  90. package/dist/component/schema.d.ts.map +1 -1
  91. package/dist/component/schema.js +18 -41
  92. package/dist/component/schema.js.map +1 -1
  93. package/dist/component/streams.d.ts +39 -339
  94. package/dist/component/streams.d.ts.map +1 -1
  95. package/dist/component/streams.js +114 -73
  96. package/dist/component/streams.js.map +1 -1
  97. package/dist/component/threads.d.ts +13 -13
  98. package/dist/component/users.d.ts +7 -7
  99. package/dist/component/vector/index.d.ts +1 -1
  100. package/dist/component/vector/index.d.ts.map +1 -1
  101. package/dist/component/vector/index.js +1 -3
  102. package/dist/component/vector/index.js.map +1 -1
  103. package/dist/deltas.d.ts +43 -0
  104. package/dist/deltas.d.ts.map +1 -0
  105. package/dist/deltas.js +446 -0
  106. package/dist/deltas.js.map +1 -0
  107. package/dist/mapping.d.ts +38 -20
  108. package/dist/mapping.d.ts.map +1 -1
  109. package/dist/mapping.js +365 -97
  110. package/dist/mapping.js.map +1 -1
  111. package/dist/react/SmoothText.d.ts +5 -0
  112. package/dist/react/SmoothText.d.ts.map +1 -0
  113. package/dist/react/SmoothText.js +6 -0
  114. package/dist/react/SmoothText.js.map +1 -0
  115. package/dist/react/index.d.ts +5 -77
  116. package/dist/react/index.d.ts.map +1 -1
  117. package/dist/react/index.js +6 -160
  118. package/dist/react/index.js.map +1 -1
  119. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  120. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  121. package/dist/react/optimisticallySendMessage.js +35 -9
  122. package/dist/react/optimisticallySendMessage.js.map +1 -1
  123. package/dist/react/types.d.ts +4 -18
  124. package/dist/react/types.d.ts.map +1 -1
  125. package/dist/react/useDeltaStreams.d.ts +10 -0
  126. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  127. package/dist/react/useDeltaStreams.js +106 -0
  128. package/dist/react/useDeltaStreams.js.map +1 -0
  129. package/dist/react/useSmoothText.d.ts +13 -12
  130. package/dist/react/useSmoothText.d.ts.map +1 -1
  131. package/dist/react/useSmoothText.js +32 -15
  132. package/dist/react/useSmoothText.js.map +1 -1
  133. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  134. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  135. package/dist/react/useStreamingUIMessages.js +92 -0
  136. package/dist/react/useStreamingUIMessages.js.map +1 -0
  137. package/dist/react/useThreadMessages.d.ts +104 -0
  138. package/dist/react/useThreadMessages.d.ts.map +1 -0
  139. package/dist/react/useThreadMessages.js +148 -0
  140. package/dist/react/useThreadMessages.js.map +1 -0
  141. package/dist/react/useUIMessages.d.ts +96 -0
  142. package/dist/react/useUIMessages.d.ts.map +1 -0
  143. package/dist/react/useUIMessages.js +108 -0
  144. package/dist/react/useUIMessages.js.map +1 -0
  145. package/dist/shared.d.ts +20 -4
  146. package/dist/shared.d.ts.map +1 -1
  147. package/dist/shared.js +45 -8
  148. package/dist/shared.js.map +1 -1
  149. package/dist/validators.d.ts +22981 -5666
  150. package/dist/validators.d.ts.map +1 -1
  151. package/dist/validators.js +245 -137
  152. package/dist/validators.js.map +1 -1
  153. package/package.json +101 -51
  154. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  155. package/src/UIMessages.test.ts +273 -0
  156. package/src/UIMessages.ts +739 -0
  157. package/src/client/approval.test.ts +350 -0
  158. package/src/client/createTool.ts +291 -76
  159. package/src/client/defaultComponent.ts +17 -0
  160. package/src/client/definePlaygroundAPI.ts +67 -31
  161. package/src/client/files.ts +100 -20
  162. package/src/client/index.test.ts +40 -85
  163. package/src/client/index.ts +638 -1289
  164. package/src/client/messages.ts +237 -0
  165. package/src/client/mockModel.ts +252 -0
  166. package/src/client/saveInputMessages.test.ts +583 -0
  167. package/src/client/saveInputMessages.ts +101 -0
  168. package/src/client/search.test.ts +1207 -0
  169. package/src/client/search.ts +581 -70
  170. package/src/client/start.ts +327 -0
  171. package/src/client/streamText.ts +187 -0
  172. package/src/client/streaming.test.ts +186 -0
  173. package/src/client/streaming.ts +241 -97
  174. package/src/client/threads.ts +83 -0
  175. package/src/client/types.ts +370 -219
  176. package/src/client/utils.ts +27 -0
  177. package/src/component/_generated/api.ts +64 -0
  178. package/src/component/_generated/component.ts +4902 -0
  179. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  180. package/src/component/files.ts +11 -2
  181. package/src/component/messages.test.ts +195 -51
  182. package/src/component/messages.ts +500 -201
  183. package/src/component/schema.ts +20 -46
  184. package/src/component/setup.test.ts +7 -0
  185. package/src/component/streams.ts +184 -83
  186. package/src/component/users.test.ts +0 -1
  187. package/src/component/vector/index.ts +1 -3
  188. package/src/deltas.test.ts +626 -0
  189. package/src/deltas.ts +569 -0
  190. package/src/fromUIMessages.test.ts +497 -0
  191. package/src/mapping.test.ts +180 -6
  192. package/src/mapping.ts +479 -162
  193. package/src/react/SmoothText.tsx +9 -0
  194. package/src/react/index.ts +10 -230
  195. package/src/react/optimisticallySendMessage.ts +55 -12
  196. package/src/react/types.ts +6 -39
  197. package/src/react/useDeltaStreams.ts +160 -0
  198. package/src/react/useSmoothText.ts +56 -36
  199. package/src/react/useStreamingUIMessages.ts +143 -0
  200. package/src/react/useThreadMessages.ts +262 -0
  201. package/src/react/useUIMessages.test.ts +255 -0
  202. package/src/react/useUIMessages.ts +195 -0
  203. package/src/shared.ts +88 -12
  204. package/src/test.ts +18 -0
  205. package/src/toUIMessages.test.ts +1269 -0
  206. package/src/validators.test.ts +18 -19
  207. package/src/validators.ts +325 -185
  208. package/dist/client/_generated/_ignore.d.ts +0 -1
  209. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  210. package/dist/client/_generated/_ignore.js +0 -3
  211. package/dist/client/_generated/_ignore.js.map +0 -1
  212. package/dist/client/listMessages.d.ts +0 -22
  213. package/dist/client/listMessages.d.ts.map +0 -1
  214. package/dist/client/listMessages.js +0 -25
  215. package/dist/client/listMessages.js.map +0 -1
  216. package/dist/package.json +0 -3
  217. package/dist/react/deltas.d.ts +0 -26
  218. package/dist/react/deltas.d.ts.map +0 -1
  219. package/dist/react/deltas.js +0 -384
  220. package/dist/react/deltas.js.map +0 -1
  221. package/dist/react/toUIMessages.d.ts +0 -15
  222. package/dist/react/toUIMessages.d.ts.map +0 -1
  223. package/dist/react/toUIMessages.js +0 -211
  224. package/dist/react/toUIMessages.js.map +0 -1
  225. package/src/client/listMessages.ts +0 -38
  226. package/src/component/_generated/api.d.ts +0 -2202
  227. package/src/component/_generated/api.js +0 -23
  228. package/src/component/_generated/server.js +0 -90
  229. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  230. package/src/react/deltas.test.ts +0 -315
  231. package/src/react/deltas.ts +0 -478
  232. package/src/react/toUIMessages.test.ts +0 -420
  233. package/src/react/toUIMessages.ts +0 -253
  234. package/src/vitest.config.ts +0 -7
  235. /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
+ }