@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
package/src/mapping.ts CHANGED
@@ -16,27 +16,43 @@ import {
16
16
  type TextPart,
17
17
  type ToolCallPart,
18
18
  type ToolResultPart,
19
- type ReasoningUIPart,
19
+ type ProviderMetadata,
20
+ type JSONValue,
20
21
  } from "ai";
21
- import type {
22
- Message,
23
- MessageWithMetadata,
24
- Usage,
25
- vContent,
26
- vFilePart,
27
- vImagePart,
28
- vReasoningPart,
29
- vRedactedReasoningPart,
30
- vTextPart,
31
- vToolCallPart,
32
- vToolResultPart,
22
+ import {
23
+ vMessageWithMetadata,
24
+ type vSourcePart,
25
+ type Message,
26
+ type MessageWithMetadata,
27
+ type Usage,
28
+ type vFilePart,
29
+ type vImagePart,
30
+ type vReasoningPart,
31
+ type vRedactedReasoningPart,
32
+ type vTextPart,
33
+ type vToolCallPart,
34
+ type vToolResultPart,
35
+ type SourcePart,
36
+ vToolResultOutput,
37
+ type MessageDoc,
38
+ vToolApprovalRequest,
39
+ vToolApprovalResponse,
33
40
  } from "./validators.js";
34
41
  import type { ActionCtx, AgentComponent } from "./client/types.js";
35
- import type { RunMutationCtx } from "./client/types.js";
42
+ import type { MutationCtx } from "./client/types.js";
36
43
  import { MAX_FILE_SIZE, storeFile } from "./client/files.js";
37
44
  import type { Infer } from "convex/values";
38
- import { omit } from "convex-helpers";
39
-
45
+ import {
46
+ convertUint8ArrayToBase64,
47
+ type ProviderOptions,
48
+ type ReasoningPart,
49
+ } from "@ai-sdk/provider-utils";
50
+ import { parse, validate } from "convex-helpers/validators";
51
+ import {
52
+ getModelName,
53
+ getProviderName,
54
+ type ModelOrMetadata,
55
+ } from "./shared.js";
40
56
  export type AIMessageWithoutId = Omit<AIMessage, "id">;
41
57
 
42
58
  export type SerializeUrlsAndUint8Arrays<T> = T extends URL
@@ -45,8 +61,7 @@ export type SerializeUrlsAndUint8Arrays<T> = T extends URL
45
61
  ? ArrayBuffer
46
62
  : T extends Array<infer Inner>
47
63
  ? Array<SerializeUrlsAndUint8Arrays<Inner>>
48
- : // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
- T extends Record<string, any>
64
+ : T extends Record<string, any>
50
65
  ? { [K in keyof T]: SerializeUrlsAndUint8Arrays<T[K]> }
51
66
  : T;
52
67
 
@@ -56,12 +71,10 @@ export type SerializedContent = Message["content"];
56
71
  export type SerializedMessage = Message;
57
72
 
58
73
  export async function serializeMessage(
59
- ctx: ActionCtx | RunMutationCtx,
74
+ ctx: ActionCtx | MutationCtx,
60
75
  component: AgentComponent,
61
- messageWithId: (ModelMessage & { id?: string }) | Message,
76
+ message: ModelMessage | Message,
62
77
  ): Promise<{ message: SerializedMessage; fileIds?: string[] }> {
63
- const message =
64
- "id" in messageWithId ? omit(messageWithId, ["id"]) : messageWithId;
65
78
  const { content, fileIds } = await serializeContent(
66
79
  ctx,
67
80
  component,
@@ -69,36 +82,129 @@ export async function serializeMessage(
69
82
  );
70
83
  return {
71
84
  message: {
72
- ...message,
85
+ role: message.role,
73
86
  content,
87
+ ...(message.providerOptions
88
+ ? { providerOptions: message.providerOptions }
89
+ : {}),
74
90
  } as SerializedMessage,
75
91
  fileIds,
76
92
  };
77
93
  }
78
94
 
79
- export function deserializeMessage(message: SerializedMessage): ModelMessage {
95
+ // Similar to serializeMessage, but doesn't save any files and is looser
96
+ // For use on the frontend / in synchronous environments.
97
+ export function fromModelMessage(message: ModelMessage): Message {
98
+ const content = fromModelMessageContent(message.content);
99
+ return {
100
+ role: message.role,
101
+ content,
102
+ ...(message.providerOptions
103
+ ? { providerOptions: message.providerOptions }
104
+ : {}),
105
+ } as SerializedMessage;
106
+ }
107
+
108
+ export async function serializeOrThrow(
109
+ message: ModelMessage | Message,
110
+ ): Promise<SerializedMessage> {
111
+ const { content } = await serializeContent(
112
+ {} as any,
113
+ {} as any,
114
+ message.content,
115
+ );
116
+ return {
117
+ role: message.role,
118
+ content,
119
+ ...(message.providerOptions
120
+ ? { providerOptions: message.providerOptions }
121
+ : {}),
122
+ } as SerializedMessage;
123
+ }
124
+
125
+ export function toModelMessage(
126
+ message: SerializedMessage | ModelMessage,
127
+ ): ModelMessage {
80
128
  return {
81
129
  ...message,
82
- content: deserializeContent(message.content),
130
+ content: toModelMessageContent(message.content),
83
131
  } as ModelMessage;
84
132
  }
85
133
 
134
+ export function docsToModelMessages(messages: MessageDoc[]): ModelMessage[] {
135
+ return messages
136
+ .map((m) => m.message)
137
+ .filter((m) => !!m)
138
+ .filter((m) => !!m.content.length)
139
+ .map(toModelMessage);
140
+ }
141
+
142
+ /**
143
+ * Merge consecutive tool messages that contain `tool-approval-response` parts
144
+ * into a single tool message. The AI SDK's `collectToolApprovals` only examines
145
+ * the last tool message, so when multiple approvals are saved as separate
146
+ * messages (e.g. approve tool A, then deny tool B), they must be combined
147
+ * for the SDK to process them all.
148
+ */
149
+ export function mergeApprovalResponseMessages(
150
+ messages: ModelMessage[],
151
+ ): ModelMessage[] {
152
+ const result: ModelMessage[] = [];
153
+ for (const msg of messages) {
154
+ const prev = result.at(-1);
155
+ if (
156
+ msg.role === "tool" &&
157
+ prev?.role === "tool" &&
158
+ Array.isArray(msg.content) &&
159
+ Array.isArray(prev.content) &&
160
+ hasApprovalResponse(msg.content) &&
161
+ hasApprovalResponse(prev.content)
162
+ ) {
163
+ // Clone before merging to avoid mutating the original message's content array
164
+ const cloned = { ...prev, content: [...(prev.content as any[])] };
165
+ result[result.length - 1] = cloned;
166
+ (cloned.content as any[]).push(...(msg.content as any[]));
167
+ } else {
168
+ result.push(msg);
169
+ }
170
+ }
171
+ return result;
172
+ }
173
+
174
+ function hasApprovalResponse(content: any[]): boolean {
175
+ return content.some(
176
+ (p: any) => p.type === "tool-approval-response",
177
+ );
178
+ }
179
+
86
180
  export function serializeUsage(usage: LanguageModelUsage): Usage {
87
181
  return {
88
182
  promptTokens: usage.inputTokens ?? 0,
89
183
  completionTokens: usage.outputTokens ?? 0,
90
184
  totalTokens: usage.totalTokens ?? 0,
91
185
  reasoningTokens: usage.reasoningTokens,
186
+ cachedInputTokens: usage.cachedInputTokens,
92
187
  };
93
188
  }
94
189
 
95
- export function deserializeUsage(usage: Usage): LanguageModelUsage {
190
+ export function toModelMessageUsage(usage: Usage): LanguageModelUsage {
96
191
  return {
97
192
  inputTokens: usage.promptTokens,
98
193
  outputTokens: usage.completionTokens,
99
194
  totalTokens: usage.totalTokens,
100
195
  reasoningTokens: usage.reasoningTokens,
101
196
  cachedInputTokens: usage.cachedInputTokens,
197
+ // These detail fields are required by LanguageModelUsage type but we don't
198
+ // have the granular data, so we provide empty objects with undefined values.
199
+ inputTokenDetails: {
200
+ cacheReadTokens: undefined,
201
+ cacheWriteTokens: undefined,
202
+ noCacheTokens: undefined,
203
+ },
204
+ outputTokenDetails: {
205
+ textTokens: undefined,
206
+ reasoningTokens: undefined,
207
+ },
102
208
  };
103
209
  }
104
210
 
@@ -109,34 +215,57 @@ export function serializeWarnings(
109
215
  return undefined;
110
216
  }
111
217
  return warnings.map((warning) => {
112
- if (warning.type !== "unsupported-setting") {
113
- return warning;
218
+ if (warning.type === "compatibility") {
219
+ return {
220
+ type: "unsupported-setting",
221
+ setting: warning.feature,
222
+ details: warning.details,
223
+ };
114
224
  }
115
- return {
116
- ...warning,
117
- setting: warning.setting.toString(),
118
- };
119
- });
225
+ return warning;
226
+ }) as any;
120
227
  }
121
228
 
122
- export function deserializeWarnings(
229
+ export function toModelMessageWarnings(
123
230
  warnings: MessageWithMetadata["warnings"],
124
231
  ): CallWarning[] | undefined {
125
- // We don't need to do anythign here for now
126
- return warnings;
232
+ if (!warnings) {
233
+ return undefined;
234
+ }
235
+ return warnings.map((warning) => {
236
+ if (warning.type === "unsupported-setting") {
237
+ return {
238
+ type: "compatibility",
239
+ feature: warning.setting,
240
+ details: warning.details,
241
+ };
242
+ }
243
+ return warning;
244
+ }) as any;
127
245
  }
128
246
 
129
247
  export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
130
248
  ctx: ActionCtx,
131
249
  component: AgentComponent,
132
250
  step: StepResult<TOOLS>,
133
- metadata: { model: string; provider: string },
134
- ): Promise<MessageWithMetadata[]> {
251
+ model: ModelOrMetadata | undefined,
252
+ /**
253
+ * If provided, these are the new response messages for this step
254
+ * (pre-sliced by the caller). When not provided, falls back to the
255
+ * existing heuristic of slicing the last 1-2 messages.
256
+ *
257
+ * This is needed for tool approval flows where the SDK adds extra
258
+ * messages (e.g. approval tool-results) at the beginning of
259
+ * responseMessages that the old slice(-1/-2) logic would miss.
260
+ */
261
+ newResponseMessages?: ModelMessage[],
262
+ ): Promise<{ messages: MessageWithMetadata[] }> {
135
263
  // If there are tool results, there's another message with the tool results
136
- // ref: https://github.com/vercel/ai/blob/main/packages/ai/core/generate-text/to-response-messages.ts
264
+ // ref: https://github.com/vercel/ai/blob/main/packages/ai/src/generate-text/to-response-messages.ts#L120
265
+ const hasToolMessage = step.response.messages.at(-1)?.role === "tool";
137
266
  const assistantFields = {
138
- model: metadata.model,
139
- provider: metadata.provider,
267
+ model: model ? getModelName(model) : undefined,
268
+ provider: model ? getProviderName(model) : undefined,
140
269
  providerMetadata: step.providerMetadata,
141
270
  reasoning: step.reasoningText,
142
271
  reasoningDetails: step.reasoning,
@@ -144,40 +273,42 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
144
273
  warnings: serializeWarnings(step.warnings),
145
274
  finishReason: step.finishReason,
146
275
  // Only store the sources on one message
147
- sources: step.toolResults.length === 0 ? step.sources : undefined,
276
+ sources: hasToolMessage ? undefined : step.sources,
148
277
  } satisfies Omit<MessageWithMetadata, "message" | "text" | "fileIds">;
149
- const toolFields = {
150
- sources: step.sources,
151
- };
278
+ const toolFields = { sources: step.sources };
279
+
280
+ // Determine which messages to serialize for this step
281
+ let messagesToSerialize: ModelMessage[];
282
+ if (newResponseMessages) {
283
+ messagesToSerialize = newResponseMessages;
284
+ } else if (hasToolMessage) {
285
+ messagesToSerialize = step.response.messages.slice(-2);
286
+ } else if (step.content.length) {
287
+ messagesToSerialize = step.response.messages.slice(-1);
288
+ } else {
289
+ messagesToSerialize = [{ role: "assistant" as const, content: [] }];
290
+ }
291
+
152
292
  const messages: MessageWithMetadata[] = await Promise.all(
153
- (step.toolResults.length > 0
154
- ? step.response.messages.slice(-2)
155
- : step.response.messages.slice(-1)
156
- ).map(async (messageWithId): Promise<MessageWithMetadata> => {
157
- const { message, fileIds } = await serializeMessage(
158
- ctx,
159
- component,
160
- messageWithId,
161
- );
162
- return {
293
+ messagesToSerialize.map(async (msg): Promise<MessageWithMetadata> => {
294
+ const { message, fileIds } = await serializeMessage(ctx, component, msg);
295
+ return parse(vMessageWithMetadata, {
163
296
  message,
164
- // Let's not store the ID by default here. It's being generated internally
165
- // and not referenced elsewhere that we know of.
166
- // id: message.id,
167
297
  ...(message.role === "tool" ? toolFields : assistantFields),
168
298
  text: step.text,
169
299
  fileIds,
170
- };
300
+ });
171
301
  }),
172
302
  );
173
- return messages;
303
+ // TODO: capture step.files separately?
304
+ return { messages };
174
305
  }
175
306
 
176
307
  export async function serializeObjectResult(
177
308
  ctx: ActionCtx,
178
309
  component: AgentComponent,
179
310
  result: GenerateObjectResult<unknown>,
180
- metadata: { model: string; provider: string },
311
+ model: ModelOrMetadata | undefined,
181
312
  ): Promise<{ messages: MessageWithMetadata[] }> {
182
313
  const text = JSON.stringify(result.object);
183
314
 
@@ -189,9 +320,8 @@ export async function serializeObjectResult(
189
320
  messages: [
190
321
  {
191
322
  message,
192
- id: result.response.id,
193
- model: metadata.model,
194
- provider: metadata.provider,
323
+ model: model ? getModelName(model) : undefined,
324
+ provider: model ? getProviderName(model) : undefined,
195
325
  providerMetadata: result.providerMetadata,
196
326
  finishReason: result.finishReason,
197
327
  text,
@@ -203,8 +333,18 @@ export async function serializeObjectResult(
203
333
  };
204
334
  }
205
335
 
336
+ function getMimeOrMediaType(part: { mediaType?: string; mimeType?: string }) {
337
+ if ("mediaType" in part) {
338
+ return part.mediaType;
339
+ }
340
+ if ("mimeType" in part) {
341
+ return part.mimeType;
342
+ }
343
+ return undefined;
344
+ }
345
+
206
346
  export async function serializeContent(
207
- ctx: ActionCtx | RunMutationCtx,
347
+ ctx: ActionCtx | MutationCtx,
208
348
  component: AgentComponent,
209
349
  content: Content | Message["content"],
210
350
  ): Promise<{ content: SerializedContent; fileIds?: string[] }> {
@@ -212,23 +352,24 @@ export async function serializeContent(
212
352
  return { content };
213
353
  }
214
354
  const fileIds: string[] = [];
215
- function getMimeType(part: { mediaType?: string; mimeType?: string }) {
216
- if ("mediaType" in part) {
217
- return part.mediaType;
218
- }
219
- if ("mimeType" in part) {
220
- return part.mimeType;
221
- }
222
- return undefined;
223
- }
224
355
  const serialized = await Promise.all(
225
356
  content.map(async (part) => {
357
+ const metadata: {
358
+ providerOptions?: ProviderOptions;
359
+ providerMetadata?: ProviderMetadata;
360
+ } = {};
361
+ if ("providerOptions" in part) {
362
+ metadata.providerOptions = part.providerOptions as ProviderOptions;
363
+ }
364
+ if ("providerMetadata" in part) {
365
+ metadata.providerMetadata = part.providerMetadata as ProviderMetadata;
366
+ }
226
367
  switch (part.type) {
227
368
  case "text": {
228
369
  return {
229
370
  type: part.type,
230
371
  text: part.text,
231
- providerOptions: part.providerOptions,
372
+ ...metadata,
232
373
  } satisfies Infer<typeof vTextPart>;
233
374
  }
234
375
  case "image": {
@@ -241,7 +382,7 @@ export async function serializeContent(
241
382
  ctx,
242
383
  component,
243
384
  new Blob([image], {
244
- type: getMimeType(part) || guessMimeType(image),
385
+ type: getMimeOrMediaType(part) || guessMimeType(image),
245
386
  }),
246
387
  );
247
388
  image = file.url;
@@ -249,8 +390,8 @@ export async function serializeContent(
249
390
  }
250
391
  return {
251
392
  type: part.type,
252
- mimeType: getMimeType(part),
253
- providerOptions: part.providerOptions,
393
+ mediaType: getMimeOrMediaType(part),
394
+ ...metadata,
254
395
  image,
255
396
  } satisfies Infer<typeof vImagePart>;
256
397
  }
@@ -260,7 +401,7 @@ export async function serializeContent(
260
401
  const { file } = await storeFile(
261
402
  ctx,
262
403
  component,
263
- new Blob([data], { type: getMimeType(part) }),
404
+ new Blob([data], { type: getMimeOrMediaType(part) }),
264
405
  );
265
406
  data = file.url;
266
407
  fileIds.push(file.fileId);
@@ -269,118 +410,295 @@ export async function serializeContent(
269
410
  type: part.type,
270
411
  data,
271
412
  filename: part.filename,
272
- mimeType: getMimeType(part)!,
273
- providerOptions: part.providerOptions,
413
+ mediaType: getMimeOrMediaType(part)!,
414
+ ...metadata,
274
415
  } satisfies Infer<typeof vFilePart>;
275
416
  }
276
417
  case "tool-call": {
277
- const args = "input" in part ? part.input : part.args;
418
+ // Handle legacy data where only args field exists
419
+ const input = part.input ?? (part as any)?.args ?? {};
278
420
  return {
279
421
  type: part.type,
280
- args: args ?? null,
422
+ input,
423
+ /** @deprecated Use `input` instead. */
424
+ args: input,
281
425
  toolCallId: part.toolCallId,
282
426
  toolName: part.toolName,
283
- providerOptions: part.providerOptions,
284
427
  providerExecuted: part.providerExecuted,
428
+ ...metadata,
285
429
  } satisfies Infer<typeof vToolCallPart>;
286
430
  }
287
431
  case "tool-result": {
288
- const result = "output" in part ? part.output : part.result;
289
- return {
290
- type: part.type,
291
- result: result ?? null,
292
- toolCallId: part.toolCallId,
293
- toolName: part.toolName,
294
- providerOptions: part.providerOptions,
295
- } satisfies Infer<typeof vToolResultPart>;
432
+ return normalizeToolResult(part, metadata);
296
433
  }
297
434
  case "reasoning": {
298
435
  return {
299
436
  type: part.type,
300
437
  text: part.text,
301
- providerOptions: part.providerOptions,
438
+ ...metadata,
302
439
  } satisfies Infer<typeof vReasoningPart>;
303
440
  }
441
+ // Not in current generation output, but could be in historical messages
304
442
  case "redacted-reasoning": {
305
443
  return {
306
444
  type: part.type,
307
445
  data: part.data,
308
- providerOptions: part.providerOptions,
446
+ ...metadata,
309
447
  } satisfies Infer<typeof vRedactedReasoningPart>;
310
448
  }
449
+ case "source": {
450
+ return part satisfies Infer<typeof vSourcePart>;
451
+ }
452
+ case "tool-approval-request": {
453
+ return {
454
+ type: part.type,
455
+ approvalId: part.approvalId,
456
+ toolCallId: part.toolCallId,
457
+ ...metadata,
458
+ } satisfies Infer<typeof vToolApprovalRequest>;
459
+ }
460
+ case "tool-approval-response": {
461
+ return {
462
+ type: part.type,
463
+ approvalId: part.approvalId,
464
+ approved: part.approved,
465
+ reason: part.reason,
466
+ providerExecuted: part.providerExecuted,
467
+ ...metadata,
468
+ } satisfies Infer<typeof vToolApprovalResponse>;
469
+ }
311
470
  default:
312
- return part satisfies Infer<typeof vContent>;
471
+ return null;
313
472
  }
314
473
  }),
315
474
  );
316
475
  return {
317
- content: serialized as SerializedContent,
476
+ content: serialized.filter((p) => p !== null) as SerializedContent,
318
477
  fileIds: fileIds.length > 0 ? fileIds : undefined,
319
478
  };
320
479
  }
321
480
 
322
- export function deserializeContent(content: SerializedContent): Content {
481
+ export function fromModelMessageContent(content: Content): Message["content"] {
323
482
  if (typeof content === "string") {
324
483
  return content;
325
484
  }
326
- return content.map((part) => {
327
- switch (part.type) {
328
- case "text":
329
- return {
330
- type: part.type,
331
- text: part.text,
332
- providerOptions: part.providerOptions,
333
- } satisfies TextPart;
334
- case "image":
335
- return {
336
- type: part.type,
337
- image: deserializeUrl(part.image),
338
- mediaType: part.mimeType,
339
- providerOptions: part.providerOptions,
340
- } satisfies ImagePart;
341
- case "file":
342
- return {
343
- type: part.type,
344
- data: deserializeUrl(part.data),
345
- filename: part.filename,
346
- mediaType: part.mimeType,
347
- providerOptions: part.providerOptions,
348
- } satisfies FilePart;
349
- case "tool-call":
350
- return {
351
- type: part.type,
352
- input: part.args ?? null,
353
- providerExecuted: part.providerExecuted,
354
- toolCallId: part.toolCallId,
355
- toolName: part.toolName,
356
- providerOptions: part.providerOptions,
357
- } satisfies ToolCallPart;
358
- case "tool-result":
359
- return {
360
- type: part.type,
361
- output: part.result ?? null,
362
- toolCallId: part.toolCallId,
363
- toolName: part.toolName,
364
- providerOptions: part.providerOptions,
365
- } satisfies ToolResultPart;
366
- case "reasoning":
367
- return {
368
- type: part.type,
369
- text: part.text,
370
- providerMetadata: part.providerOptions,
371
- state: part.state,
372
- } satisfies ReasoningUIPart;
373
- case "redacted-reasoning":
374
- // TODO: should we just drop this?
375
- return {
376
- type: "reasoning",
377
- text: part.data,
378
- providerMetadata: part.providerOptions,
379
- } satisfies ReasoningUIPart;
380
- default:
381
- return part satisfies Content;
382
- }
383
- }) as Content;
485
+ return content
486
+ .map((part) => {
487
+ const metadata: {
488
+ providerOptions?: ProviderOptions;
489
+ providerMetadata?: ProviderMetadata;
490
+ } = {};
491
+ if ("providerOptions" in part) {
492
+ metadata.providerOptions = part.providerOptions as ProviderOptions;
493
+ }
494
+ if ("providerMetadata" in part) {
495
+ metadata.providerMetadata = part.providerMetadata as ProviderMetadata;
496
+ }
497
+ switch (part.type) {
498
+ case "text":
499
+ return part satisfies Infer<typeof vTextPart>;
500
+ case "image":
501
+ return {
502
+ type: part.type,
503
+ mediaType: getMimeOrMediaType(part),
504
+ ...metadata,
505
+ image: serializeDataOrUrl(part.image),
506
+ } satisfies Infer<typeof vImagePart>;
507
+ case "file":
508
+ return {
509
+ type: part.type,
510
+ data: serializeDataOrUrl(part.data),
511
+ filename: part.filename,
512
+ mediaType: getMimeOrMediaType(part)!,
513
+ ...metadata,
514
+ } satisfies Infer<typeof vFilePart>;
515
+ case "tool-call":
516
+ // Handle legacy data where only args field exists
517
+ return {
518
+ type: part.type,
519
+ input: part.input ?? (part as any)?.args ?? {},
520
+ /** @deprecated Use `input` instead. */
521
+ args: part.input ?? (part as any)?.args ?? {},
522
+ toolCallId: part.toolCallId,
523
+ toolName: part.toolName,
524
+ providerExecuted: part.providerExecuted,
525
+ ...metadata,
526
+ } satisfies Infer<typeof vToolCallPart>;
527
+ case "tool-result":
528
+ return normalizeToolResult(part, metadata);
529
+ case "reasoning":
530
+ return {
531
+ type: part.type,
532
+ text: part.text,
533
+ ...metadata,
534
+ } satisfies Infer<typeof vReasoningPart>;
535
+ case "tool-approval-request":
536
+ return {
537
+ type: part.type,
538
+ approvalId: part.approvalId,
539
+ toolCallId: part.toolCallId,
540
+ ...metadata,
541
+ } satisfies Infer<typeof vToolApprovalRequest>;
542
+ case "tool-approval-response":
543
+ return {
544
+ type: part.type,
545
+ approvalId: part.approvalId,
546
+ approved: part.approved,
547
+ reason: part.reason,
548
+ providerExecuted: part.providerExecuted,
549
+ ...metadata,
550
+ } satisfies Infer<typeof vToolApprovalResponse>;
551
+ // Not in current generation output, but could be in historical messages
552
+ default:
553
+ return null;
554
+ }
555
+ })
556
+ .filter((p) => p !== null) as Message["content"];
557
+ }
558
+
559
+ export function toModelMessageContent(
560
+ content: SerializedContent | ModelMessage["content"],
561
+ ): Content {
562
+ if (typeof content === "string") {
563
+ return content;
564
+ }
565
+ return content
566
+ .map((part) => {
567
+ const metadata: {
568
+ providerOptions?: ProviderOptions;
569
+ providerMetadata?: ProviderMetadata;
570
+ } = {};
571
+ if ("providerOptions" in part) {
572
+ metadata.providerOptions = part.providerOptions;
573
+ }
574
+ if ("providerMetadata" in part) {
575
+ metadata.providerMetadata = part.providerMetadata;
576
+ }
577
+ switch (part.type) {
578
+ case "text":
579
+ return {
580
+ type: part.type,
581
+ text: part.text,
582
+ ...metadata,
583
+ } satisfies TextPart;
584
+ case "image":
585
+ return {
586
+ type: part.type,
587
+ image: toModelMessageDataOrUrl(part.image),
588
+ mediaType: getMimeOrMediaType(part),
589
+ ...metadata,
590
+ } satisfies ImagePart;
591
+ case "file":
592
+ return {
593
+ type: part.type,
594
+ data: toModelMessageDataOrUrl(part.data),
595
+ filename: part.filename,
596
+ mediaType: getMimeOrMediaType(part)!,
597
+ ...metadata,
598
+ } satisfies FilePart;
599
+ case "tool-call": {
600
+ // Handle legacy data where only args field exists
601
+ const input = part.input ?? (part as any)?.args ?? {};
602
+ return {
603
+ type: part.type,
604
+ input,
605
+ toolCallId: part.toolCallId,
606
+ toolName: part.toolName,
607
+ providerExecuted: part.providerExecuted,
608
+ ...metadata,
609
+ } satisfies ToolCallPart;
610
+ }
611
+ case "tool-result": {
612
+ return normalizeToolResult(part, metadata);
613
+ }
614
+ case "reasoning":
615
+ return {
616
+ type: part.type,
617
+ text: part.text,
618
+ ...metadata,
619
+ } satisfies ReasoningPart;
620
+ case "redacted-reasoning":
621
+ // TODO: should we just drop this?
622
+ return {
623
+ type: "reasoning",
624
+ text: "",
625
+ ...metadata,
626
+ providerOptions: metadata.providerOptions
627
+ ? {
628
+ ...Object.fromEntries(
629
+ Object.entries(metadata.providerOptions ?? {}).map(
630
+ ([key, value]) => [
631
+ key,
632
+ { ...value, redactedData: part.data },
633
+ ],
634
+ ),
635
+ ),
636
+ }
637
+ : undefined,
638
+ } satisfies ReasoningPart;
639
+ case "source":
640
+ return part satisfies SourcePart;
641
+ case "tool-approval-request":
642
+ return {
643
+ type: part.type,
644
+ approvalId: part.approvalId,
645
+ toolCallId: part.toolCallId,
646
+ ...metadata,
647
+ } satisfies Infer<typeof vToolApprovalRequest>;
648
+ case "tool-approval-response":
649
+ return {
650
+ type: part.type,
651
+ approvalId: part.approvalId,
652
+ approved: part.approved,
653
+ reason: part.reason,
654
+ providerExecuted: part.providerExecuted,
655
+ ...metadata,
656
+ } satisfies Infer<typeof vToolApprovalResponse>;
657
+ default:
658
+ return null;
659
+ }
660
+ })
661
+ .filter((p) => p !== null) as Content;
662
+ }
663
+
664
+ export function normalizeToolOutput(
665
+ result: string | JSONValue | undefined,
666
+ ): ToolResultPart["output"] {
667
+ if (typeof result === "string") {
668
+ return {
669
+ type: "text",
670
+ value: result,
671
+ };
672
+ }
673
+ if (validate(vToolResultOutput, result)) {
674
+ return result;
675
+ }
676
+ return {
677
+ type: "json",
678
+ value: result ?? null,
679
+ };
680
+ }
681
+
682
+ function normalizeToolResult(
683
+ part: ToolResultPart | Infer<typeof vToolResultPart>,
684
+ metadata: {
685
+ providerOptions?: ProviderOptions;
686
+ providerMetadata?: ProviderMetadata;
687
+ },
688
+ ): ToolResultPart & Infer<typeof vToolResultPart> {
689
+ return {
690
+ type: part.type,
691
+ output: part.output
692
+ ? validate(vToolResultOutput, part.output)
693
+ ? (part.output as any)
694
+ : normalizeToolOutput(JSON.stringify(part.output))
695
+ : normalizeToolOutput("result" in part ? part.result : undefined),
696
+ toolCallId: part.toolCallId,
697
+ toolName: part.toolName,
698
+ // Preserve isError flag for error reporting
699
+ ...("isError" in part && part.isError ? { isError: true } : {}),
700
+ ...metadata,
701
+ } satisfies ToolResultPart;
384
702
  }
385
703
 
386
704
  /**
@@ -473,9 +791,12 @@ export function serializeDataOrUrl(
473
791
  ) as ArrayBuffer;
474
792
  }
475
793
 
476
- export function deserializeUrl(
477
- urlOrString: string | ArrayBuffer,
794
+ export function toModelMessageDataOrUrl(
795
+ urlOrString: string | ArrayBuffer | URL | DataContent,
478
796
  ): URL | DataContent {
797
+ if (urlOrString instanceof URL) {
798
+ return urlOrString;
799
+ }
479
800
  if (typeof urlOrString === "string") {
480
801
  if (
481
802
  urlOrString.startsWith("http://") ||
@@ -492,7 +813,7 @@ export function toUIFilePart(part: ImagePart | FilePart): FileUIPart {
492
813
  const dataOrUrl = part.type === "image" ? part.image : part.data;
493
814
  const url =
494
815
  dataOrUrl instanceof ArrayBuffer
495
- ? encodeBase64(dataOrUrl)
816
+ ? convertUint8ArrayToBase64(new Uint8Array(dataOrUrl))
496
817
  : dataOrUrl.toString();
497
818
 
498
819
  return {
@@ -504,10 +825,6 @@ export function toUIFilePart(part: ImagePart | FilePart): FileUIPart {
504
825
  };
505
826
  }
506
827
 
507
- function encodeBase64(data: ArrayBuffer): string {
508
- return Buffer.from(data).toString("base64");
509
- }
510
-
511
828
  // Currently unused
512
829
  // export function toModelMessages(args: {
513
830
  // messages?: ModelMessage[] | AIMessageWithoutId[];