@convex-dev/agent 0.6.4 → 0.7.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.
- package/MIGRATION.md +60 -116
- package/dist/client/messages.d.ts +1 -398
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +3 -47
- package/dist/client/messages.js.map +1 -1
- package/dist/client/types.d.ts +1 -455
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +805 -45
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/messages.d.ts +1252 -201
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +49 -23
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +2593 -332
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/streams.d.ts +7 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +24 -30
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts.map +1 -1
- package/dist/component/threads.js +1 -2
- package/dist/component/threads.js.map +1 -1
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -5
- package/dist/react/index.js.map +1 -1
- package/dist/shared.d.ts +19 -7
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +4 -4
- package/dist/shared.js.map +1 -1
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.js +475 -0
- package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
- package/dist/validators.d.ts +10026 -1055
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +71 -9
- package/dist/validators.js.map +1 -1
- package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
- package/dist/vercel/UIMessages.d.ts.map +1 -0
- package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
- package/dist/vercel/UIMessages.js.map +1 -0
- package/dist/{client → vercel/client}/createTool.d.ts +23 -16
- package/dist/vercel/client/createTool.d.ts.map +1 -0
- package/dist/{client → vercel/client}/createTool.js +2 -0
- package/dist/vercel/client/createTool.js.map +1 -0
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +533 -79
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/{client → vercel/client}/definePlaygroundAPI.js +8 -7
- package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
- package/dist/{client → vercel/client}/files.d.ts +2 -2
- package/dist/vercel/client/files.d.ts.map +1 -0
- package/dist/vercel/client/files.js.map +1 -0
- package/dist/vercel/client/messages.d.ts +542 -0
- package/dist/vercel/client/messages.d.ts.map +1 -0
- package/dist/vercel/client/messages.js +66 -0
- package/dist/vercel/client/messages.js.map +1 -0
- package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
- package/dist/vercel/client/mockModel.d.ts.map +1 -0
- package/dist/{client → vercel/client}/mockModel.js +12 -13
- package/dist/vercel/client/mockModel.js.map +1 -0
- package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
- package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
- package/dist/vercel/client/saveInputMessages.js.map +1 -0
- package/dist/{client → vercel/client}/search.d.ts +119 -22
- package/dist/vercel/client/search.d.ts.map +1 -0
- package/dist/{client → vercel/client}/search.js +2 -2
- package/dist/vercel/client/search.js.map +1 -0
- package/dist/{client → vercel/client}/start.d.ts +26 -11
- package/dist/vercel/client/start.d.ts.map +1 -0
- package/dist/{client → vercel/client}/start.js +64 -22
- package/dist/vercel/client/start.js.map +1 -0
- package/dist/{client → vercel/client}/streamText.d.ts +13 -12
- package/dist/vercel/client/streamText.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streamText.js +43 -9
- package/dist/vercel/client/streamText.js.map +1 -0
- package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
- package/dist/vercel/client/streaming.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streaming.js +120 -52
- package/dist/vercel/client/streaming.js.map +1 -0
- package/dist/vercel/client/types.d.ts +456 -0
- package/dist/vercel/client/types.d.ts.map +1 -0
- package/dist/vercel/client/types.js +3 -0
- package/dist/vercel/client/types.js.map +1 -0
- package/dist/{client → vercel/client}/utils.d.ts +3 -2
- package/dist/vercel/client/utils.d.ts.map +1 -0
- package/dist/{client → vercel/client}/utils.js +2 -2
- package/dist/vercel/client/utils.js.map +1 -0
- package/dist/{deltas.d.ts → vercel/deltas.d.ts} +3 -4
- package/dist/vercel/deltas.d.ts.map +1 -0
- package/dist/{deltas.js → vercel/deltas.js} +195 -99
- package/dist/vercel/deltas.js.map +1 -0
- package/dist/{client → vercel}/index.d.ts +482 -106
- package/dist/vercel/index.d.ts.map +1 -0
- package/dist/{client → vercel}/index.js +64 -46
- package/dist/vercel/index.js.map +1 -0
- package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -19
- package/dist/vercel/mapping.d.ts.map +1 -0
- package/dist/{mapping.js → vercel/mapping.js} +411 -114
- package/dist/vercel/mapping.js.map +1 -0
- package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
- package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
- package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
- package/dist/{react → vercel/react}/types.d.ts +1 -1
- package/dist/vercel/react/types.d.ts.map +1 -0
- package/dist/{react → vercel/react}/types.js.map +1 -1
- package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
- package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
- package/dist/vercel/react/useDeltaStreams.js.map +1 -0
- package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
- package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useStreamingUIMessages.js +7 -3
- package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
- package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
- package/dist/vercel/react/useThreadMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.d.ts +1 -1
- package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.js +1 -1
- package/dist/vercel/react/useUIMessages.js.map +1 -0
- package/package.json +31 -24
- package/src/client/messages.ts +5 -107
- package/src/client/types.ts +0 -608
- package/src/component/_generated/component.ts +1089 -48
- package/src/component/messages.test.ts +273 -1
- package/src/component/messages.ts +62 -32
- package/src/component/streams.ts +47 -47
- package/src/component/threads.ts +5 -2
- package/src/eslintConfig.test.ts +44 -0
- package/src/react/index.ts +8 -5
- package/src/shared.ts +26 -36
- package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
- package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
- package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
- package/src/streaming/persistedUIMessageChunks.ts +658 -0
- package/src/validators.test.ts +4 -1
- package/src/validators.ts +89 -8
- package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
- package/src/{UIMessages.ts → vercel/UIMessages.ts} +95 -3
- package/src/{client → vercel/client}/approval.test.ts +23 -13
- package/src/{client → vercel/client}/createTool.ts +62 -22
- package/src/{client → vercel/client}/definePlaygroundAPI.ts +15 -16
- package/src/{client → vercel/client}/files.ts +2 -2
- package/src/{client → vercel/client}/index.test.ts +99 -31
- package/src/vercel/client/messages.ts +182 -0
- package/src/{client → vercel/client}/mockModel.ts +38 -39
- package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
- package/src/{client → vercel/client}/saveInputMessages.ts +3 -3
- package/src/{client → vercel/client}/search.test.ts +3 -3
- package/src/{client → vercel/client}/search.ts +4 -4
- package/src/{client → vercel/client}/setup.test.ts +6 -3
- package/src/vercel/client/start.test.ts +14 -0
- package/src/{client → vercel/client}/start.ts +147 -36
- package/src/vercel/client/streamText.test.ts +223 -0
- package/src/{client → vercel/client}/streamText.ts +102 -31
- package/src/{client → vercel/client}/streaming.integration.test.ts +6 -3
- package/src/{client → vercel/client}/streaming.test.ts +178 -4
- package/src/{client → vercel/client}/streaming.ts +114 -52
- package/src/vercel/client/types.test.ts +13 -0
- package/src/vercel/client/types.ts +679 -0
- package/src/{client → vercel/client}/utils.ts +13 -7
- package/src/{deltas.test.ts → vercel/deltas.test.ts} +384 -93
- package/src/{deltas.ts → vercel/deltas.ts} +218 -115
- package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
- package/src/{client → vercel}/index.ts +167 -84
- package/src/{mapping.test.ts → vercel/mapping.test.ts} +377 -92
- package/src/{mapping.ts → vercel/mapping.ts} +501 -137
- package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
- package/src/{react → vercel/react}/types.ts +1 -1
- package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
- package/src/{react → vercel/react}/useStreamingUIMessages.ts +7 -3
- package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
- package/src/{react → vercel/react}/useUIMessages.ts +2 -2
- package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
- package/dist/UIMessages.d.ts.map +0 -1
- package/dist/UIMessages.js.map +0 -1
- package/dist/client/createTool.d.ts.map +0 -1
- package/dist/client/createTool.js.map +0 -1
- package/dist/client/definePlaygroundAPI.js.map +0 -1
- package/dist/client/files.d.ts.map +0 -1
- package/dist/client/files.js.map +0 -1
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js.map +0 -1
- package/dist/client/mockModel.d.ts.map +0 -1
- package/dist/client/mockModel.js.map +0 -1
- package/dist/client/saveInputMessages.d.ts.map +0 -1
- package/dist/client/saveInputMessages.js.map +0 -1
- package/dist/client/search.d.ts.map +0 -1
- package/dist/client/search.js.map +0 -1
- package/dist/client/start.d.ts.map +0 -1
- package/dist/client/start.js.map +0 -1
- package/dist/client/streamText.d.ts.map +0 -1
- package/dist/client/streamText.js.map +0 -1
- package/dist/client/streaming.d.ts.map +0 -1
- package/dist/client/streaming.js.map +0 -1
- package/dist/client/utils.d.ts.map +0 -1
- package/dist/client/utils.js.map +0 -1
- package/dist/deltas.d.ts.map +0 -1
- package/dist/deltas.js.map +0 -1
- package/dist/mapping.d.ts.map +0 -1
- package/dist/mapping.js.map +0 -1
- package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
- package/dist/react/optimisticallySendMessage.js.map +0 -1
- package/dist/react/types.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.js.map +0 -1
- package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
- package/dist/react/useStreamingUIMessages.js.map +0 -1
- package/dist/react/useThreadMessages.d.ts.map +0 -1
- package/dist/react/useThreadMessages.js.map +0 -1
- package/dist/react/useUIMessages.d.ts.map +0 -1
- package/dist/react/useUIMessages.js.map +0 -1
- package/src/client/streamText.test.ts +0 -102
- /package/dist/{client → vercel/client}/files.js +0 -0
- /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
- /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
- /package/dist/{react → vercel/react}/types.js +0 -0
- /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
- /package/src/{react → vercel/react}/useUIMessages.test.ts +0 -0
|
@@ -4,23 +4,33 @@ import {
|
|
|
4
4
|
serializeDataOrUrl,
|
|
5
5
|
toModelMessageDataOrUrl,
|
|
6
6
|
serializeMessage,
|
|
7
|
-
|
|
7
|
+
serializeResponseMessages,
|
|
8
8
|
toModelMessage,
|
|
9
9
|
serializeContent,
|
|
10
10
|
toModelMessageContent,
|
|
11
|
+
toUIFilePart,
|
|
11
12
|
autoDenyUnresolvedApprovals,
|
|
13
|
+
serializeWarnings,
|
|
12
14
|
} from "./mapping.js";
|
|
13
|
-
import { api } from "
|
|
15
|
+
import { api } from "../component/_generated/api.js";
|
|
14
16
|
import type { AgentComponent, ActionCtx } from "./client/types.js";
|
|
15
|
-
import { vMessage, vToolResultPart } from "
|
|
17
|
+
import { vMessage, vToolResultPart } from "../validators.js";
|
|
16
18
|
import fs from "fs";
|
|
17
19
|
import path from "path";
|
|
18
20
|
import type { SerializedContent } from "./mapping.js";
|
|
19
21
|
import { validate } from "convex-helpers/validators";
|
|
20
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
FilePart,
|
|
24
|
+
ModelMessage,
|
|
25
|
+
StepResult,
|
|
26
|
+
ToolResultPart,
|
|
27
|
+
ToolSet,
|
|
28
|
+
CallWarning,
|
|
29
|
+
} from "ai";
|
|
21
30
|
import type { Infer } from "convex/values";
|
|
31
|
+
import { mockModel } from "./client/mockModel.js";
|
|
22
32
|
|
|
23
|
-
const testAssetsDir = path.join(__dirname, "
|
|
33
|
+
const testAssetsDir = path.join(__dirname, "../../test-assets");
|
|
24
34
|
const testFiles = [
|
|
25
35
|
"book.svg",
|
|
26
36
|
"bump.jpeg",
|
|
@@ -36,6 +46,21 @@ function fileToArrayBuffer(filePath: string): ArrayBuffer {
|
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
describe("mapping", () => {
|
|
49
|
+
test("serializes every AI SDK 7 warning discriminant", () => {
|
|
50
|
+
const warnings: CallWarning[] = [
|
|
51
|
+
{ type: "unsupported", feature: "feature", details: "details" },
|
|
52
|
+
{ type: "compatibility", feature: "feature", details: "details" },
|
|
53
|
+
{
|
|
54
|
+
type: "deprecated",
|
|
55
|
+
setting: "setting",
|
|
56
|
+
message: "message",
|
|
57
|
+
},
|
|
58
|
+
{ type: "other", message: "message" },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
expect(serializeWarnings(warnings)).toEqual(warnings);
|
|
62
|
+
});
|
|
63
|
+
|
|
39
64
|
test("infers correct mimeType for all test-assets", () => {
|
|
40
65
|
const expected: { [key: string]: string } = {
|
|
41
66
|
"book.svg": "image/svg+xml", // <svg
|
|
@@ -135,6 +160,266 @@ describe("mapping", () => {
|
|
|
135
160
|
expect(serialized).toMatchObject(expected);
|
|
136
161
|
});
|
|
137
162
|
|
|
163
|
+
test("legacy result JSON with a type field is not treated as SDK output", async () => {
|
|
164
|
+
const weather = { type: "weather", temperature: 72 };
|
|
165
|
+
const toolResult = {
|
|
166
|
+
type: "tool-result" as const,
|
|
167
|
+
toolCallId: "tool-call-id",
|
|
168
|
+
toolName: "weather",
|
|
169
|
+
result: weather,
|
|
170
|
+
} satisfies Infer<typeof vToolResultPart>;
|
|
171
|
+
const expected = { type: "json", value: weather };
|
|
172
|
+
|
|
173
|
+
const [deserialized] = toModelMessageContent([
|
|
174
|
+
toolResult,
|
|
175
|
+
]) as ToolResultPart[];
|
|
176
|
+
expect(deserialized.output).toEqual(expected);
|
|
177
|
+
const { content } = await serializeContent(
|
|
178
|
+
{} as ActionCtx,
|
|
179
|
+
{} as AgentComponent,
|
|
180
|
+
[toolResult],
|
|
181
|
+
);
|
|
182
|
+
expect((content[0] as Infer<typeof vToolResultPart>).output).toEqual(
|
|
183
|
+
expected,
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test.each([
|
|
188
|
+
[
|
|
189
|
+
"tagged data",
|
|
190
|
+
{
|
|
191
|
+
type: "file",
|
|
192
|
+
data: { type: "data", data: new Uint8Array([1, 2]) },
|
|
193
|
+
mediaType: "application/octet-stream",
|
|
194
|
+
},
|
|
195
|
+
{ type: "data" },
|
|
196
|
+
],
|
|
197
|
+
[
|
|
198
|
+
"tagged URL",
|
|
199
|
+
{
|
|
200
|
+
type: "file",
|
|
201
|
+
data: { type: "url", url: new URL("https://example.com/file") },
|
|
202
|
+
mediaType: "application/pdf",
|
|
203
|
+
},
|
|
204
|
+
{ type: "url", url: new URL("https://example.com/file") },
|
|
205
|
+
],
|
|
206
|
+
[
|
|
207
|
+
"tagged text",
|
|
208
|
+
{
|
|
209
|
+
type: "file",
|
|
210
|
+
data: { type: "text", text: "hello" },
|
|
211
|
+
mediaType: "text/plain",
|
|
212
|
+
},
|
|
213
|
+
{ type: "text", text: "hello" },
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
"tagged reference",
|
|
217
|
+
{
|
|
218
|
+
type: "file",
|
|
219
|
+
data: { type: "reference", reference: { openai: "file-1" } },
|
|
220
|
+
mediaType: "application/pdf",
|
|
221
|
+
},
|
|
222
|
+
{ type: "reference", reference: { openai: "file-1" } },
|
|
223
|
+
],
|
|
224
|
+
[
|
|
225
|
+
"legacy file-data",
|
|
226
|
+
{
|
|
227
|
+
type: "file-data",
|
|
228
|
+
data: "AQI=",
|
|
229
|
+
mediaType: "application/octet-stream",
|
|
230
|
+
},
|
|
231
|
+
{ type: "data", data: "AQI=" },
|
|
232
|
+
],
|
|
233
|
+
[
|
|
234
|
+
"legacy file-url",
|
|
235
|
+
{
|
|
236
|
+
type: "file-url",
|
|
237
|
+
url: "https://example.com/file",
|
|
238
|
+
mediaType: "application/pdf",
|
|
239
|
+
},
|
|
240
|
+
{ type: "url", url: new URL("https://example.com/file") },
|
|
241
|
+
],
|
|
242
|
+
[
|
|
243
|
+
"legacy file ID",
|
|
244
|
+
{ type: "file-id", fileId: "file-1" },
|
|
245
|
+
{ type: "file-id", fileId: "file-1" },
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
"provider-keyed legacy file ID",
|
|
249
|
+
{ type: "file-id", fileId: { openai: "file-1" } },
|
|
250
|
+
{ type: "reference", reference: { openai: "file-1" } },
|
|
251
|
+
],
|
|
252
|
+
[
|
|
253
|
+
"legacy file reference",
|
|
254
|
+
{ type: "file-reference", providerReference: { openai: "file-1" } },
|
|
255
|
+
{ type: "reference", reference: { openai: "file-1" } },
|
|
256
|
+
],
|
|
257
|
+
[
|
|
258
|
+
"legacy image data",
|
|
259
|
+
{ type: "image-data", data: "AQI=", mediaType: "image/png" },
|
|
260
|
+
{ type: "data", data: "AQI=" },
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
"legacy image URL",
|
|
264
|
+
{ type: "image-url", url: "https://example.com/image" },
|
|
265
|
+
{ type: "url", url: new URL("https://example.com/image") },
|
|
266
|
+
],
|
|
267
|
+
[
|
|
268
|
+
"legacy image ID",
|
|
269
|
+
{ type: "image-file-id", fileId: "image-1" },
|
|
270
|
+
{ type: "image-file-id", fileId: "image-1" },
|
|
271
|
+
],
|
|
272
|
+
[
|
|
273
|
+
"provider-keyed legacy image ID",
|
|
274
|
+
{ type: "image-file-id", fileId: { openai: "image-1" } },
|
|
275
|
+
{ type: "reference", reference: { openai: "image-1" } },
|
|
276
|
+
],
|
|
277
|
+
[
|
|
278
|
+
"legacy image reference",
|
|
279
|
+
{
|
|
280
|
+
type: "image-file-reference",
|
|
281
|
+
providerReference: { openai: "image-1" },
|
|
282
|
+
},
|
|
283
|
+
{ type: "reference", reference: { openai: "image-1" } },
|
|
284
|
+
],
|
|
285
|
+
])("round-trips AI SDK 7 tool-result %s", async (_name, value, expected) => {
|
|
286
|
+
const result = {
|
|
287
|
+
type: "tool-result",
|
|
288
|
+
toolCallId: "call-1",
|
|
289
|
+
toolName: "lookup",
|
|
290
|
+
output: { type: "content", value: [value] },
|
|
291
|
+
} as ToolResultPart;
|
|
292
|
+
const { content } = await serializeContent(
|
|
293
|
+
{} as ActionCtx,
|
|
294
|
+
{} as AgentComponent,
|
|
295
|
+
[result],
|
|
296
|
+
);
|
|
297
|
+
const [restored] = toModelMessageContent(content) as ToolResultPart[];
|
|
298
|
+
const restoredPart = (restored.output as { value: unknown[] }).value[0]!;
|
|
299
|
+
if ("fileId" in expected) {
|
|
300
|
+
expect(restoredPart).toMatchObject(expected);
|
|
301
|
+
} else {
|
|
302
|
+
expect((restoredPart as { data: unknown }).data).toMatchObject(expected);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("deserializes persisted legacy media tool output to a canonical file", () => {
|
|
307
|
+
const [restored] = toModelMessageContent([
|
|
308
|
+
{
|
|
309
|
+
type: "tool-result",
|
|
310
|
+
toolCallId: "call-1",
|
|
311
|
+
toolName: "render",
|
|
312
|
+
output: {
|
|
313
|
+
type: "content",
|
|
314
|
+
value: [{ type: "media", data: "AQI=", mediaType: "image/png" }],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
]) as ToolResultPart[];
|
|
318
|
+
expect(restored.output).toEqual({
|
|
319
|
+
type: "content",
|
|
320
|
+
value: [
|
|
321
|
+
{
|
|
322
|
+
type: "file",
|
|
323
|
+
data: { type: "data", data: "AQI=" },
|
|
324
|
+
mediaType: "image/png",
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("reasoning files persist tagged data and URLs, and reject missing data", async () => {
|
|
331
|
+
const { content } = await serializeContent(
|
|
332
|
+
{} as ActionCtx,
|
|
333
|
+
{} as AgentComponent,
|
|
334
|
+
[
|
|
335
|
+
{
|
|
336
|
+
type: "reasoning-file",
|
|
337
|
+
data: { type: "url", url: new URL("https://example.com/reasoning") },
|
|
338
|
+
mediaType: "text/plain",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
type: "reasoning-file",
|
|
342
|
+
data: { type: "data", data: new Uint8Array([1, 2]) },
|
|
343
|
+
mediaType: "application/octet-stream",
|
|
344
|
+
},
|
|
345
|
+
{ type: "custom", kind: "provider.annotation" },
|
|
346
|
+
] as ModelMessage["content"],
|
|
347
|
+
);
|
|
348
|
+
expect(content).toMatchObject([
|
|
349
|
+
{ type: "reasoning-file", url: "https://example.com/reasoning" },
|
|
350
|
+
{ type: "reasoning-file", data: expect.any(ArrayBuffer) },
|
|
351
|
+
{ type: "custom", kind: "provider.annotation" },
|
|
352
|
+
]);
|
|
353
|
+
expect(toModelMessageContent(content)).toMatchObject([
|
|
354
|
+
{
|
|
355
|
+
type: "reasoning-file",
|
|
356
|
+
data: { type: "url", url: new URL("https://example.com/reasoning") },
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
type: "reasoning-file",
|
|
360
|
+
data: { type: "data", data: expect.any(ArrayBuffer) },
|
|
361
|
+
},
|
|
362
|
+
{ type: "custom", kind: "provider.annotation" },
|
|
363
|
+
]);
|
|
364
|
+
expect(
|
|
365
|
+
(await serializeContent({} as ActionCtx, {} as AgentComponent, content))
|
|
366
|
+
.content,
|
|
367
|
+
).toEqual(content);
|
|
368
|
+
expect(() =>
|
|
369
|
+
toModelMessageContent([
|
|
370
|
+
{ type: "reasoning-file", mediaType: "text/plain" },
|
|
371
|
+
] as SerializedContent),
|
|
372
|
+
).toThrow("reasoning-file requires data or url");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("reserializing persisted provider fields preserves them", async () => {
|
|
376
|
+
const content: SerializedContent = [
|
|
377
|
+
{ type: "reasoning", text: "private", signature: "signed" },
|
|
378
|
+
{
|
|
379
|
+
type: "tool-result",
|
|
380
|
+
toolCallId: "provider-call",
|
|
381
|
+
toolName: "search",
|
|
382
|
+
providerExecuted: true,
|
|
383
|
+
output: { type: "text", value: "found" },
|
|
384
|
+
},
|
|
385
|
+
];
|
|
386
|
+
await expect(
|
|
387
|
+
serializeContent({} as ActionCtx, {} as AgentComponent, content),
|
|
388
|
+
).resolves.toEqual({ content, fileIds: undefined });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test.each([
|
|
392
|
+
[
|
|
393
|
+
"ArrayBuffer",
|
|
394
|
+
{
|
|
395
|
+
type: "file",
|
|
396
|
+
data: new Uint8Array([1, 2]).buffer,
|
|
397
|
+
mediaType: "application/octet-stream",
|
|
398
|
+
},
|
|
399
|
+
{ url: "data:application/octet-stream;base64,AQI=" },
|
|
400
|
+
],
|
|
401
|
+
[
|
|
402
|
+
"tagged data",
|
|
403
|
+
{
|
|
404
|
+
type: "file",
|
|
405
|
+
data: { type: "data", data: new Uint8Array([1, 2]) },
|
|
406
|
+
mediaType: "application/octet-stream",
|
|
407
|
+
},
|
|
408
|
+
{ url: "data:application/octet-stream;base64,AQI=" },
|
|
409
|
+
],
|
|
410
|
+
[
|
|
411
|
+
"provider reference",
|
|
412
|
+
{
|
|
413
|
+
type: "file",
|
|
414
|
+
data: { type: "reference", reference: { openai: "file-1" } },
|
|
415
|
+
mediaType: "application/pdf",
|
|
416
|
+
},
|
|
417
|
+
{ url: "", providerReference: { openai: "file-1" } },
|
|
418
|
+
],
|
|
419
|
+
])("renders file UI part for %s", (_name, part, expected) => {
|
|
420
|
+
expect(toUIFilePart(part as FilePart)).toMatchObject(expected);
|
|
421
|
+
});
|
|
422
|
+
|
|
138
423
|
test("saving files returns fileIds when too big", async () => {
|
|
139
424
|
// Make a big file
|
|
140
425
|
const bigArr = new Uint8Array(1024 * 65).fill(1);
|
|
@@ -225,6 +510,7 @@ describe("mapping", () => {
|
|
|
225
510
|
);
|
|
226
511
|
expect(content).toHaveLength(1);
|
|
227
512
|
expect((content as unknown[])[0]).toMatchObject(approvalRequest);
|
|
513
|
+
expect(toModelMessageContent(content)).toMatchObject([approvalRequest]);
|
|
228
514
|
});
|
|
229
515
|
|
|
230
516
|
test("tool-approval-response with approved: true is preserved", async () => {
|
|
@@ -260,7 +546,23 @@ describe("mapping", () => {
|
|
|
260
546
|
expect((content as unknown[])[0]).toMatchObject(approvalResponse);
|
|
261
547
|
});
|
|
262
548
|
|
|
263
|
-
|
|
549
|
+
test("stored reasoning-file URLs survive serialization", async () => {
|
|
550
|
+
const reasoningFile: SerializedContent = [
|
|
551
|
+
{
|
|
552
|
+
type: "reasoning-file",
|
|
553
|
+
url: "https://example.com/reasoning",
|
|
554
|
+
mediaType: "text/plain",
|
|
555
|
+
},
|
|
556
|
+
];
|
|
557
|
+
const { content } = await serializeContent(
|
|
558
|
+
{} as ActionCtx,
|
|
559
|
+
{} as AgentComponent,
|
|
560
|
+
reasoningFile,
|
|
561
|
+
);
|
|
562
|
+
expect(content).toEqual(reasoningFile);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
describe("serializeResponseMessages", () => {
|
|
264
566
|
const ctx = {
|
|
265
567
|
runAction: async () => undefined,
|
|
266
568
|
runMutation: async () => undefined,
|
|
@@ -276,7 +578,12 @@ describe("mapping", () => {
|
|
|
276
578
|
{
|
|
277
579
|
role: "assistant",
|
|
278
580
|
content: [
|
|
279
|
-
{
|
|
581
|
+
{
|
|
582
|
+
type: "tool-call",
|
|
583
|
+
toolCallId: "c1",
|
|
584
|
+
toolName: "search",
|
|
585
|
+
input: {},
|
|
586
|
+
},
|
|
280
587
|
],
|
|
281
588
|
},
|
|
282
589
|
{
|
|
@@ -292,15 +599,18 @@ describe("mapping", () => {
|
|
|
292
599
|
},
|
|
293
600
|
];
|
|
294
601
|
const step1Messages: ModelMessage[] = [
|
|
295
|
-
...step0Messages,
|
|
296
602
|
{ role: "assistant", content: [{ type: "text", text: "thinking" }] },
|
|
297
603
|
];
|
|
298
604
|
const step2Messages: ModelMessage[] = [
|
|
299
|
-
...step1Messages,
|
|
300
605
|
{
|
|
301
606
|
role: "assistant",
|
|
302
607
|
content: [
|
|
303
|
-
{
|
|
608
|
+
{
|
|
609
|
+
type: "tool-call",
|
|
610
|
+
toolCallId: "c2",
|
|
611
|
+
toolName: "search",
|
|
612
|
+
input: {},
|
|
613
|
+
},
|
|
304
614
|
],
|
|
305
615
|
},
|
|
306
616
|
{
|
|
@@ -350,13 +660,24 @@ describe("mapping", () => {
|
|
|
350
660
|
return c.map((p: { type?: string }) => p.type ?? "?");
|
|
351
661
|
};
|
|
352
662
|
|
|
353
|
-
test("
|
|
354
|
-
const res = await
|
|
663
|
+
test("explicitly provided empty response messages stay empty", async () => {
|
|
664
|
+
const res = await serializeResponseMessages(
|
|
665
|
+
ctx,
|
|
666
|
+
component,
|
|
667
|
+
makeStep([]),
|
|
668
|
+
undefined,
|
|
669
|
+
[],
|
|
670
|
+
);
|
|
671
|
+
expect(res.messages).toEqual([]);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test("serializes all response messages for a step", async () => {
|
|
675
|
+
const res = await serializeResponseMessages(
|
|
355
676
|
ctx,
|
|
356
677
|
component,
|
|
357
678
|
makeStep(step0Messages),
|
|
358
679
|
undefined,
|
|
359
|
-
|
|
680
|
+
step0Messages,
|
|
360
681
|
);
|
|
361
682
|
expect(res.messages).toHaveLength(2);
|
|
362
683
|
expect(res.messages[0].message.role).toBe("assistant");
|
|
@@ -365,26 +686,50 @@ describe("mapping", () => {
|
|
|
365
686
|
expect(contentTypes(res.messages[1].message)).toEqual(["tool-result"]);
|
|
366
687
|
});
|
|
367
688
|
|
|
368
|
-
test("
|
|
369
|
-
const res = await
|
|
689
|
+
test("serializes the text response from a later step", async () => {
|
|
690
|
+
const res = await serializeResponseMessages(
|
|
370
691
|
ctx,
|
|
371
692
|
component,
|
|
372
693
|
makeStep(step1Messages),
|
|
373
694
|
undefined,
|
|
374
|
-
|
|
695
|
+
step1Messages,
|
|
375
696
|
);
|
|
376
697
|
expect(res.messages).toHaveLength(1);
|
|
377
698
|
expect(res.messages[0].message.role).toBe("assistant");
|
|
378
699
|
expect(contentTypes(res.messages[0].message)).toEqual(["text"]);
|
|
379
700
|
});
|
|
380
701
|
|
|
381
|
-
test("
|
|
382
|
-
const
|
|
702
|
+
test("persists the model that generated an SDK 7 step over the fallback", async () => {
|
|
703
|
+
const routedModel = mockModel({
|
|
704
|
+
provider: "routed-provider",
|
|
705
|
+
modelId: "routed-model",
|
|
706
|
+
});
|
|
707
|
+
const step = {
|
|
708
|
+
...makeStep(step1Messages),
|
|
709
|
+
model: routedModel,
|
|
710
|
+
} as StepResult<ToolSet>;
|
|
711
|
+
|
|
712
|
+
const res = await serializeResponseMessages(
|
|
713
|
+
ctx,
|
|
714
|
+
component,
|
|
715
|
+
step,
|
|
716
|
+
{ provider: "fallback-provider", model: "fallback-model" },
|
|
717
|
+
step1Messages,
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
expect(res.messages[0]).toMatchObject({
|
|
721
|
+
model: "routed-model",
|
|
722
|
+
provider: "routed-provider",
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("serializes a tool-call and tool-result from the same step", async () => {
|
|
727
|
+
const res = await serializeResponseMessages(
|
|
383
728
|
ctx,
|
|
384
729
|
component,
|
|
385
730
|
makeStep(step2Messages),
|
|
386
731
|
undefined,
|
|
387
|
-
|
|
732
|
+
step2Messages,
|
|
388
733
|
);
|
|
389
734
|
expect(res.messages).toHaveLength(2);
|
|
390
735
|
expect(res.messages[0].message.role).toBe("assistant");
|
|
@@ -393,19 +738,21 @@ describe("mapping", () => {
|
|
|
393
738
|
expect(contentTypes(res.messages[1].message)).toEqual(["tool-result"]);
|
|
394
739
|
});
|
|
395
740
|
|
|
396
|
-
|
|
397
|
-
// assistant(text) + assistant(tool-call) + tool(tool-result), so the new
|
|
398
|
-
// tail has length 3 and the last message is a tool message. The old
|
|
399
|
-
// heuristic took `slice(-2)` whenever the last role was "tool" and would
|
|
400
|
-
// have dropped the leading text. The watermark returns all three.
|
|
401
|
-
test("returns all three messages when a step adds text + tool-call + tool-result", async () => {
|
|
741
|
+
test("preserves text, a tool call, and its result from one step", async () => {
|
|
402
742
|
const stepMessages: ModelMessage[] = [
|
|
403
|
-
|
|
404
|
-
|
|
743
|
+
{
|
|
744
|
+
role: "assistant",
|
|
745
|
+
content: [{ type: "text", text: "Let me check..." }],
|
|
746
|
+
},
|
|
405
747
|
{
|
|
406
748
|
role: "assistant",
|
|
407
749
|
content: [
|
|
408
|
-
{
|
|
750
|
+
{
|
|
751
|
+
type: "tool-call",
|
|
752
|
+
toolCallId: "c3",
|
|
753
|
+
toolName: "search",
|
|
754
|
+
input: {},
|
|
755
|
+
},
|
|
409
756
|
],
|
|
410
757
|
},
|
|
411
758
|
{
|
|
@@ -420,12 +767,12 @@ describe("mapping", () => {
|
|
|
420
767
|
],
|
|
421
768
|
},
|
|
422
769
|
];
|
|
423
|
-
const res = await
|
|
770
|
+
const res = await serializeResponseMessages(
|
|
424
771
|
ctx,
|
|
425
772
|
component,
|
|
426
773
|
makeStep(stepMessages),
|
|
427
774
|
undefined,
|
|
428
|
-
|
|
775
|
+
stepMessages,
|
|
429
776
|
);
|
|
430
777
|
expect(res.messages).toHaveLength(3);
|
|
431
778
|
expect(res.messages[0].message.role).toBe("assistant");
|
|
@@ -435,68 +782,6 @@ describe("mapping", () => {
|
|
|
435
782
|
expect(res.messages[2].message.role).toBe("tool");
|
|
436
783
|
expect(contentTypes(res.messages[2].message)).toEqual(["tool-result"]);
|
|
437
784
|
});
|
|
438
|
-
|
|
439
|
-
test("empty response messages slice falls back to synthetic empty assistant", async () => {
|
|
440
|
-
const res = await serializeNewMessagesInStep(
|
|
441
|
-
ctx,
|
|
442
|
-
component,
|
|
443
|
-
makeStep(step1Messages),
|
|
444
|
-
undefined,
|
|
445
|
-
step1Messages.length,
|
|
446
|
-
);
|
|
447
|
-
expect(res.messages).toHaveLength(1);
|
|
448
|
-
expect(res.messages[0].message.role).toBe("assistant");
|
|
449
|
-
expect(res.messages[0].message.content).toEqual([]);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
// Pin the caller-drift behavior: if the watermark is past the end of
|
|
453
|
-
// response.messages (e.g. the caller mistracked), the slice is empty and
|
|
454
|
-
// we fall through to the synthetic anchor. Future "fixes" should not
|
|
455
|
-
// accidentally change this without intent.
|
|
456
|
-
test("watermark beyond response.messages.length returns the synthetic fallback", async () => {
|
|
457
|
-
const res = await serializeNewMessagesInStep(
|
|
458
|
-
ctx,
|
|
459
|
-
component,
|
|
460
|
-
makeStep(step1Messages),
|
|
461
|
-
undefined,
|
|
462
|
-
step1Messages.length + 5,
|
|
463
|
-
);
|
|
464
|
-
expect(res.messages).toHaveLength(1);
|
|
465
|
-
expect(res.messages[0].message.role).toBe("assistant");
|
|
466
|
-
expect(res.messages[0].message.content).toEqual([]);
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// AI SDK v6 makes step.response.messages cumulative across steps:
|
|
470
|
-
// step N's array contains all messages from steps 0..N. Without the
|
|
471
|
-
// previousResponseMessageCount watermark, every multi-step save duplicates
|
|
472
|
-
// all prior messages. These tests demonstrate the bug and the fix.
|
|
473
|
-
describe("multi-step loop — previousStep watermark", () => {
|
|
474
|
-
test("without watermark, step 2 re-saves all cumulative messages (demonstrates the bug)", async () => {
|
|
475
|
-
// step2Messages = step0 (2 msgs) + step1 (1 msg) + step2 new (2 msgs) = 5 total
|
|
476
|
-
const res = await serializeNewMessagesInStep(
|
|
477
|
-
ctx,
|
|
478
|
-
component,
|
|
479
|
-
makeStep(step2Messages),
|
|
480
|
-
undefined,
|
|
481
|
-
0,
|
|
482
|
-
);
|
|
483
|
-
expect(res.messages).toHaveLength(5);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
test("with watermark, step 2 saves only its 2 new messages", async () => {
|
|
487
|
-
const step1 = makeStep(step1Messages);
|
|
488
|
-
const res = await serializeNewMessagesInStep(
|
|
489
|
-
ctx,
|
|
490
|
-
component,
|
|
491
|
-
makeStep(step2Messages),
|
|
492
|
-
undefined,
|
|
493
|
-
step1.response.messages.length,
|
|
494
|
-
);
|
|
495
|
-
expect(res.messages).toHaveLength(2);
|
|
496
|
-
expect(contentTypes(res.messages[0].message)).toEqual(["tool-call"]);
|
|
497
|
-
expect(contentTypes(res.messages[1].message)).toEqual(["tool-result"]);
|
|
498
|
-
});
|
|
499
|
-
});
|
|
500
785
|
});
|
|
501
786
|
|
|
502
787
|
describe("autoDenyUnresolvedApprovals", () => {
|