@convex-dev/agent 0.6.2 → 0.6.4
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/dist/UIMessages.d.ts.map +1 -1
- package/dist/UIMessages.js.map +1 -1
- package/dist/client/definePlaygroundAPI.d.ts +69 -64
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +8 -5
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/index.d.ts +12 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +11 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/saveInputMessages.d.ts.map +1 -1
- package/dist/client/saveInputMessages.js.map +1 -1
- package/dist/client/types.d.ts +3 -3
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/apiKeys.js +5 -5
- package/dist/component/apiKeys.js.map +1 -1
- package/dist/component/convex.config.d.ts +1 -1
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +13 -11
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +37 -27
- package/dist/component/messages.js.map +1 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +22 -17
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.js +7 -7
- package/dist/component/threads.js.map +1 -1
- package/dist/component/users.js +2 -2
- package/dist/component/users.js.map +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +14 -8
- package/dist/component/vector/index.js.map +1 -1
- package/dist/deltas.d.ts +16 -27
- package/dist/deltas.d.ts.map +1 -1
- package/dist/deltas.js +269 -286
- package/dist/deltas.js.map +1 -1
- package/dist/mapping.d.ts +9 -3
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +16 -14
- package/dist/mapping.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.js +42 -26
- package/dist/react/useStreamingUIMessages.js.map +1 -1
- package/dist/react/useUIMessages.d.ts +1 -0
- package/dist/react/useUIMessages.d.ts.map +1 -1
- package/dist/react/useUIMessages.js +7 -3
- package/dist/react/useUIMessages.js.map +1 -1
- package/package.json +7 -6
- package/src/UIMessages.ts +1 -2
- package/src/client/approval.test.ts +25 -6
- package/src/client/createTool.ts +1 -1
- package/src/client/definePlaygroundAPI.ts +33 -17
- package/src/client/index.test.ts +91 -0
- package/src/client/index.ts +25 -1
- package/src/client/saveInputMessages.ts +4 -1
- package/src/client/streaming.integration.test.ts +39 -117
- package/src/client/types.ts +6 -21
- package/src/component/apiKeys.ts +5 -5
- package/src/component/files.test.ts +1 -1
- package/src/component/files.ts +14 -12
- package/src/component/messages.ts +40 -28
- package/src/component/streams.ts +33 -17
- package/src/component/threads.ts +7 -7
- package/src/component/users.ts +2 -2
- package/src/component/vector/index.ts +14 -7
- package/src/deltas.test.ts +373 -392
- package/src/deltas.ts +339 -378
- package/src/mapping.test.ts +296 -18
- package/src/mapping.ts +17 -11
- package/src/react/useStreamingUIMessages.ts +62 -34
- package/src/react/useUIMessages.test.ts +80 -1
- package/src/react/useUIMessages.ts +11 -3
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ApiFromModules,
|
|
7
7
|
type GenericActionCtx,
|
|
8
8
|
type GenericDataModel,
|
|
9
|
+
type GenericMutationCtx,
|
|
9
10
|
type GenericQueryCtx,
|
|
10
11
|
} from "convex/server";
|
|
11
12
|
import { v } from "convex/values";
|
|
@@ -64,7 +65,10 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
async function validateApiKey(
|
|
68
|
+
async function validateApiKey(
|
|
69
|
+
ctx: QueryCtx | MutationCtx | ActionCtx,
|
|
70
|
+
apiKey: string,
|
|
71
|
+
) {
|
|
68
72
|
await ctx.runQuery(component.apiKeys.validate, { apiKey });
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -276,18 +280,22 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
|
|
|
276
280
|
},
|
|
277
281
|
{ contextOptions, storageOptions, saveStreamDeltas: true },
|
|
278
282
|
);
|
|
279
|
-
const outputMessages =
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
283
|
+
const outputMessages: MessageDoc[][] = [];
|
|
284
|
+
let previousResponseMessageCount = 0;
|
|
285
|
+
for (const step of await steps) {
|
|
286
|
+
const { messages } = await serializeNewMessagesInStep(
|
|
287
|
+
ctx,
|
|
288
|
+
component,
|
|
289
|
+
step,
|
|
290
|
+
{
|
|
291
|
+
model: getModelName(agent.options.languageModel),
|
|
292
|
+
provider: getProviderName(agent.options.languageModel),
|
|
293
|
+
},
|
|
294
|
+
previousResponseMessageCount,
|
|
295
|
+
);
|
|
296
|
+
previousResponseMessageCount = step.response.messages.length;
|
|
297
|
+
outputMessages.push(
|
|
298
|
+
messages.map((messageWithMetadata, i) => {
|
|
291
299
|
return {
|
|
292
300
|
...messageWithMetadata,
|
|
293
301
|
tool: isTool(messageWithMetadata.message),
|
|
@@ -300,9 +308,9 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
|
|
|
300
308
|
order: 0,
|
|
301
309
|
stepOrder: i + 1,
|
|
302
310
|
} satisfies MessageDoc;
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
306
314
|
return { text: await text, messages: outputMessages.flat() };
|
|
307
315
|
},
|
|
308
316
|
returns: v.object({ text: v.string(), messages: v.array(vMessageDoc) }),
|
|
@@ -367,4 +375,12 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
|
|
|
367
375
|
};
|
|
368
376
|
}
|
|
369
377
|
|
|
370
|
-
type
|
|
378
|
+
type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
|
|
379
|
+
type MutationCtx = Pick<
|
|
380
|
+
GenericMutationCtx<GenericDataModel>,
|
|
381
|
+
"runQuery" | "runMutation"
|
|
382
|
+
>;
|
|
383
|
+
type ActionCtx = Pick<
|
|
384
|
+
GenericActionCtx<GenericDataModel>,
|
|
385
|
+
"runQuery" | "runMutation" | "runAction"
|
|
386
|
+
>;
|
package/src/client/index.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
Agent,
|
|
4
4
|
createThread,
|
|
5
|
+
createTool,
|
|
5
6
|
filterOutOrphanedToolMessages,
|
|
6
7
|
type MessageDoc,
|
|
7
8
|
} from "./index.js";
|
|
@@ -62,6 +63,74 @@ export const createThreadManually = mutation({
|
|
|
62
63
|
},
|
|
63
64
|
});
|
|
64
65
|
|
|
66
|
+
const saveStepAgent = new Agent(components.agent, {
|
|
67
|
+
name: "save-step-test",
|
|
68
|
+
instructions: "test",
|
|
69
|
+
tools: {
|
|
70
|
+
echo: createTool({
|
|
71
|
+
description: "Echo a value",
|
|
72
|
+
inputSchema: z.object({ value: z.string() }),
|
|
73
|
+
execute: async (_ctx, input) => `echo:${input.value}`,
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
languageModel: mockModel({
|
|
77
|
+
contentSteps: [
|
|
78
|
+
[
|
|
79
|
+
{
|
|
80
|
+
type: "tool-call",
|
|
81
|
+
toolCallId: "ss-1",
|
|
82
|
+
toolName: "echo",
|
|
83
|
+
input: JSON.stringify({ value: "hi" }),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
[{ type: "text", text: "done" }],
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
stopWhen: stepCountIs(5),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const replayStepsViaSaveStep = action({
|
|
93
|
+
args: { withWatermark: v.boolean() },
|
|
94
|
+
handler: async (ctx, args) => {
|
|
95
|
+
const { thread } = await saveStepAgent.createThread(ctx, {
|
|
96
|
+
userId: "ss-gen",
|
|
97
|
+
});
|
|
98
|
+
const genResult = await thread.generateText({ prompt: "echo hi" });
|
|
99
|
+
const steps = genResult.steps;
|
|
100
|
+
|
|
101
|
+
const { threadId } = await saveStepAgent.createThread(ctx, {
|
|
102
|
+
userId: "ss-replay",
|
|
103
|
+
});
|
|
104
|
+
const { messageId: promptMessageId } = await saveStepAgent.saveMessage(ctx, {
|
|
105
|
+
threadId,
|
|
106
|
+
message: { role: "user", content: "echo hi" },
|
|
107
|
+
skipEmbeddings: true,
|
|
108
|
+
});
|
|
109
|
+
let previousStep: (typeof steps)[number] | undefined;
|
|
110
|
+
for (const step of steps) {
|
|
111
|
+
await saveStepAgent.saveStep(ctx, {
|
|
112
|
+
threadId,
|
|
113
|
+
promptMessageId,
|
|
114
|
+
step,
|
|
115
|
+
previousStep: args.withWatermark ? previousStep : undefined,
|
|
116
|
+
});
|
|
117
|
+
previousStep = step;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const replayed = await saveStepAgent.listMessages(ctx, {
|
|
121
|
+
threadId,
|
|
122
|
+
paginationOpts: { cursor: null, numItems: 50 },
|
|
123
|
+
statuses: ["success", "pending", "failed"],
|
|
124
|
+
});
|
|
125
|
+
const contentTypes = replayed.page.flatMap((m) =>
|
|
126
|
+
Array.isArray(m.message?.content)
|
|
127
|
+
? m.message!.content.map((c: { type?: string }) => c.type ?? "text")
|
|
128
|
+
: ["text"],
|
|
129
|
+
);
|
|
130
|
+
return { stepCount: steps.length, contentTypes };
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
65
134
|
export const createThreadMutation = agent.createThreadMutation();
|
|
66
135
|
export const generateObjectAction = agent.asObjectAction({
|
|
67
136
|
schema: z.object({ hello: z.string().describe("A string for testing") }),
|
|
@@ -162,6 +231,7 @@ const testApi: ApiFromModules<{
|
|
|
162
231
|
generateTextAction: typeof generateTextAction;
|
|
163
232
|
generateObjectAction: typeof generateObjectAction;
|
|
164
233
|
saveMessageMutation: typeof saveMessageMutation;
|
|
234
|
+
replayStepsViaSaveStep: typeof replayStepsViaSaveStep;
|
|
165
235
|
};
|
|
166
236
|
}>["fns"] = anyApi["index.test"] as any;
|
|
167
237
|
|
|
@@ -177,6 +247,27 @@ describe("Agent thick client", () => {
|
|
|
177
247
|
expect(result).toBeDefined();
|
|
178
248
|
expect(result).toMatch(TEST_TEXT);
|
|
179
249
|
});
|
|
250
|
+
test("saveStep with previousStep saves each step's new messages exactly once", async () => {
|
|
251
|
+
const t = initConvexTest(schema);
|
|
252
|
+
const res = await t.action(testApi.replayStepsViaSaveStep, {
|
|
253
|
+
withWatermark: true,
|
|
254
|
+
});
|
|
255
|
+
expect(res.stepCount).toBe(2);
|
|
256
|
+
const toolCalls = res.contentTypes.filter((t) => t === "tool-call").length;
|
|
257
|
+
const toolResults = res.contentTypes.filter(
|
|
258
|
+
(t) => t === "tool-result",
|
|
259
|
+
).length;
|
|
260
|
+
expect(toolCalls).toBe(1);
|
|
261
|
+
expect(toolResults).toBe(1);
|
|
262
|
+
});
|
|
263
|
+
test("saveStep without previousStep duplicates prior messages", async () => {
|
|
264
|
+
const t = initConvexTest(schema);
|
|
265
|
+
const res = await t.action(testApi.replayStepsViaSaveStep, {
|
|
266
|
+
withWatermark: false,
|
|
267
|
+
});
|
|
268
|
+
const toolCalls = res.contentTypes.filter((t) => t === "tool-call").length;
|
|
269
|
+
expect(toolCalls).toBeGreaterThan(1);
|
|
270
|
+
});
|
|
180
271
|
});
|
|
181
272
|
|
|
182
273
|
describe("filterOutOrphanedToolMessages", () => {
|
package/src/client/index.ts
CHANGED
|
@@ -1182,7 +1182,9 @@ export class Agent<
|
|
|
1182
1182
|
}
|
|
1183
1183
|
|
|
1184
1184
|
/**
|
|
1185
|
-
* Explicitly save a "step" created by the AI SDK.
|
|
1185
|
+
* Explicitly save a "step" created by the AI SDK. For multi-step generation
|
|
1186
|
+
* loops, pass `previousStep` so we save only the new response messages —
|
|
1187
|
+
* see the arg JSDoc for why.
|
|
1186
1188
|
* @param ctx The ctx argument to a mutation or action.
|
|
1187
1189
|
* @param args The Step generated by the AI SDK.
|
|
1188
1190
|
*/
|
|
@@ -1199,6 +1201,15 @@ export class Agent<
|
|
|
1199
1201
|
* The step to save, possibly including multiple tool calls.
|
|
1200
1202
|
*/
|
|
1201
1203
|
step: StepResult<TOOLS>;
|
|
1204
|
+
/**
|
|
1205
|
+
* The previous step in the same generation loop, if any. Pass it so we
|
|
1206
|
+
* can compute how many of `step.response.messages` are already saved.
|
|
1207
|
+
* Omit for the first step. AI SDK v6's `step.response.messages` is
|
|
1208
|
+
* cumulative across steps; without this, multi-step callers duplicate
|
|
1209
|
+
* every prior message on every save — the exact failure mode this fix
|
|
1210
|
+
* addresses, just at the public-API layer.
|
|
1211
|
+
*/
|
|
1212
|
+
previousStep?: StepResult<TOOLS>;
|
|
1202
1213
|
/**
|
|
1203
1214
|
* The model used to generate the step.
|
|
1204
1215
|
* Defaults to the chat model for the Agent.
|
|
@@ -1211,6 +1222,18 @@ export class Agent<
|
|
|
1211
1222
|
provider?: string;
|
|
1212
1223
|
},
|
|
1213
1224
|
): Promise<{ messages: MessageDoc[] }> {
|
|
1225
|
+
const previousResponseMessageCount =
|
|
1226
|
+
args.previousStep?.response.messages.length ?? 0;
|
|
1227
|
+
if (
|
|
1228
|
+
args.previousStep !== undefined &&
|
|
1229
|
+
args.step.response.messages.length < previousResponseMessageCount
|
|
1230
|
+
) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
`saveStep: step.response.messages length (${args.step.response.messages.length}) is less than ` +
|
|
1233
|
+
`previousStep.response.messages length (${previousResponseMessageCount}). ` +
|
|
1234
|
+
`Ensure previousStep is from the immediately preceding step in the same generation loop.`,
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1214
1237
|
const { messages } = await serializeNewMessagesInStep(
|
|
1215
1238
|
ctx,
|
|
1216
1239
|
this.component,
|
|
@@ -1219,6 +1242,7 @@ export class Agent<
|
|
|
1219
1242
|
provider: args.provider ?? getProviderName(this.options.languageModel),
|
|
1220
1243
|
model: args.model ?? getModelName(this.options.languageModel),
|
|
1221
1244
|
},
|
|
1245
|
+
previousResponseMessageCount,
|
|
1222
1246
|
);
|
|
1223
1247
|
const embeddings = await this.generateEmbeddings(
|
|
1224
1248
|
ctx,
|
|
@@ -31,7 +31,10 @@ export async function saveInputMessages(
|
|
|
31
31
|
storageOptions?: {
|
|
32
32
|
saveMessages?: "all" | "promptAndOutput";
|
|
33
33
|
};
|
|
34
|
-
} & Pick<
|
|
34
|
+
} & Pick<
|
|
35
|
+
Config,
|
|
36
|
+
"usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings"
|
|
37
|
+
>,
|
|
35
38
|
): Promise<{
|
|
36
39
|
promptMessageId: string | undefined;
|
|
37
40
|
pendingMessage: MessageDoc;
|
|
@@ -9,11 +9,7 @@ import {
|
|
|
9
9
|
DeltaStreamer,
|
|
10
10
|
mergeTransforms,
|
|
11
11
|
} from "./streaming.js";
|
|
12
|
-
import {
|
|
13
|
-
getParts,
|
|
14
|
-
deriveUIMessagesFromDeltas,
|
|
15
|
-
deriveUIMessagesFromTextStreamParts,
|
|
16
|
-
} from "../deltas.js";
|
|
12
|
+
import { getParts, deriveUIMessagesFromDeltas } from "../deltas.js";
|
|
17
13
|
import type { TestConvex } from "convex-test";
|
|
18
14
|
import type { StreamDelta, StreamMessage } from "../validators.js";
|
|
19
15
|
import { dedupeMessages } from "../react/useUIMessages.js";
|
|
@@ -152,9 +148,7 @@ describe("HTTP Streaming Initiation", () => {
|
|
|
152
148
|
|
|
153
149
|
// Verify we can reconstruct the text from deltas
|
|
154
150
|
const { parts } = getParts(deltas);
|
|
155
|
-
const textParts = parts.filter(
|
|
156
|
-
(p: any) => p.type === "text-delta",
|
|
157
|
-
);
|
|
151
|
+
const textParts = parts.filter((p: any) => p.type === "text-delta");
|
|
158
152
|
expect(textParts.length).toBeGreaterThan(0);
|
|
159
153
|
});
|
|
160
154
|
});
|
|
@@ -265,10 +259,9 @@ describe("Stream Exclusion Logic", () => {
|
|
|
265
259
|
await streamer2.addParts([{ type: "start" }]);
|
|
266
260
|
|
|
267
261
|
// Default list: only streaming
|
|
268
|
-
const defaultStreams = await ctx.runQuery(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
);
|
|
262
|
+
const defaultStreams = await ctx.runQuery(components.agent.streams.list, {
|
|
263
|
+
threadId,
|
|
264
|
+
});
|
|
272
265
|
expect(defaultStreams).toHaveLength(1);
|
|
273
266
|
expect(defaultStreams[0].status).toBe("streaming");
|
|
274
267
|
expect(defaultStreams[0].order).toBe(1);
|
|
@@ -325,10 +318,10 @@ describe("Stream Exclusion Logic", () => {
|
|
|
325
318
|
expect(finishedStreams[0].status).toBe("finished");
|
|
326
319
|
|
|
327
320
|
// Query for only aborted
|
|
328
|
-
const abortedStreams = await ctx.runQuery(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
);
|
|
321
|
+
const abortedStreams = await ctx.runQuery(components.agent.streams.list, {
|
|
322
|
+
threadId,
|
|
323
|
+
statuses: ["aborted"],
|
|
324
|
+
});
|
|
332
325
|
expect(abortedStreams).toHaveLength(1);
|
|
333
326
|
expect(abortedStreams[0].status).toBe("aborted");
|
|
334
327
|
|
|
@@ -487,10 +480,10 @@ describe("Delta Stream Consumption", () => {
|
|
|
487
480
|
expect(laterParts.length).toBeLessThanOrEqual(allParts.length);
|
|
488
481
|
|
|
489
482
|
// Fetching from the end cursor should yield nothing
|
|
490
|
-
const noDeltas = await ctx.runQuery(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
);
|
|
483
|
+
const noDeltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
484
|
+
threadId,
|
|
485
|
+
cursors: [{ cursor: endCursor, streamId }],
|
|
486
|
+
});
|
|
494
487
|
expect(noDeltas).toHaveLength(0);
|
|
495
488
|
});
|
|
496
489
|
});
|
|
@@ -526,16 +519,13 @@ describe("Delta Stream Consumption", () => {
|
|
|
526
519
|
const id2 = streamer2.streamId!;
|
|
527
520
|
|
|
528
521
|
// Fetch deltas for both streams simultaneously
|
|
529
|
-
const deltas = await ctx.runQuery(
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
],
|
|
537
|
-
},
|
|
538
|
-
);
|
|
522
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
523
|
+
threadId,
|
|
524
|
+
cursors: [
|
|
525
|
+
{ cursor: 0, streamId: id1 },
|
|
526
|
+
{ cursor: 0, streamId: id2 },
|
|
527
|
+
],
|
|
528
|
+
});
|
|
539
529
|
|
|
540
530
|
// Should have deltas for both streams
|
|
541
531
|
const s1Deltas = deltas.filter((d) => d.streamId === id1);
|
|
@@ -568,10 +558,10 @@ describe("Delta Stream Consumption", () => {
|
|
|
568
558
|
threadId,
|
|
569
559
|
statuses: ["finished"],
|
|
570
560
|
});
|
|
571
|
-
const deltas = await ctx.runQuery(
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
);
|
|
561
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
562
|
+
threadId,
|
|
563
|
+
cursors: [{ cursor: 0, streamId }],
|
|
564
|
+
});
|
|
575
565
|
|
|
576
566
|
// Derive UI messages
|
|
577
567
|
const uiMessages = await deriveUIMessagesFromDeltas(
|
|
@@ -615,10 +605,10 @@ describe("Delta Stream Consumption", () => {
|
|
|
615
605
|
await streamer.consumeStream(result.toUIMessageStream());
|
|
616
606
|
const streamId = streamer.streamId!;
|
|
617
607
|
|
|
618
|
-
const deltas = await ctx.runQuery(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
);
|
|
608
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
609
|
+
threadId,
|
|
610
|
+
cursors: [{ cursor: 0, streamId }],
|
|
611
|
+
});
|
|
622
612
|
const { parts } = getParts(deltas);
|
|
623
613
|
|
|
624
614
|
// Compressed: all text-deltas for one text section should be merged
|
|
@@ -677,77 +667,6 @@ describe("Delta Stream Consumption", () => {
|
|
|
677
667
|
expect((parts[0] as { type: string }).type).toBe("new");
|
|
678
668
|
expect(cursor).toBe(6);
|
|
679
669
|
});
|
|
680
|
-
|
|
681
|
-
test("TextStreamPart format delta reconstruction with tool calls", () => {
|
|
682
|
-
const streamId = "s1";
|
|
683
|
-
const streamMessage: StreamMessage = {
|
|
684
|
-
streamId,
|
|
685
|
-
order: 1,
|
|
686
|
-
stepOrder: 0,
|
|
687
|
-
status: "streaming",
|
|
688
|
-
};
|
|
689
|
-
const deltas: StreamDelta[] = [
|
|
690
|
-
{
|
|
691
|
-
streamId,
|
|
692
|
-
start: 0,
|
|
693
|
-
end: 1,
|
|
694
|
-
parts: [{ type: "text-delta", id: "txt-0", text: "Let me call a tool. " }],
|
|
695
|
-
},
|
|
696
|
-
{
|
|
697
|
-
streamId,
|
|
698
|
-
start: 1,
|
|
699
|
-
end: 2,
|
|
700
|
-
parts: [
|
|
701
|
-
{
|
|
702
|
-
type: "tool-call",
|
|
703
|
-
toolCallId: "tc1",
|
|
704
|
-
toolName: "search",
|
|
705
|
-
input: { query: "hello" },
|
|
706
|
-
},
|
|
707
|
-
],
|
|
708
|
-
},
|
|
709
|
-
{
|
|
710
|
-
streamId,
|
|
711
|
-
start: 2,
|
|
712
|
-
end: 3,
|
|
713
|
-
parts: [
|
|
714
|
-
{
|
|
715
|
-
type: "tool-result",
|
|
716
|
-
toolCallId: "tc1",
|
|
717
|
-
toolName: "search",
|
|
718
|
-
output: "Found 3 results",
|
|
719
|
-
},
|
|
720
|
-
],
|
|
721
|
-
},
|
|
722
|
-
{
|
|
723
|
-
streamId,
|
|
724
|
-
start: 3,
|
|
725
|
-
end: 4,
|
|
726
|
-
parts: [
|
|
727
|
-
{ type: "text-delta", id: "txt-1", text: "Here are the results." },
|
|
728
|
-
],
|
|
729
|
-
},
|
|
730
|
-
];
|
|
731
|
-
|
|
732
|
-
const [messages, , changed] = deriveUIMessagesFromTextStreamParts(
|
|
733
|
-
"thread1",
|
|
734
|
-
[streamMessage],
|
|
735
|
-
[],
|
|
736
|
-
deltas,
|
|
737
|
-
);
|
|
738
|
-
|
|
739
|
-
expect(messages).toHaveLength(1);
|
|
740
|
-
expect(changed).toBe(true);
|
|
741
|
-
|
|
742
|
-
const msg = messages[0];
|
|
743
|
-
expect(msg.text).toContain("Let me call a tool.");
|
|
744
|
-
expect(msg.text).toContain("Here are the results.");
|
|
745
|
-
|
|
746
|
-
const toolParts = msg.parts.filter((p: any) =>
|
|
747
|
-
p.type.startsWith("tool-"),
|
|
748
|
-
);
|
|
749
|
-
expect(toolParts.length).toBeGreaterThan(0);
|
|
750
|
-
});
|
|
751
670
|
});
|
|
752
671
|
|
|
753
672
|
// ============================================================================
|
|
@@ -879,18 +798,21 @@ describe("Fallback Behavior", () => {
|
|
|
879
798
|
order: 0,
|
|
880
799
|
stepOrder: 0,
|
|
881
800
|
status: "streaming",
|
|
801
|
+
format: "UIMessageChunk",
|
|
882
802
|
};
|
|
883
803
|
const finishedMsg: StreamMessage = {
|
|
884
804
|
streamId: "s2",
|
|
885
805
|
order: 1,
|
|
886
806
|
stepOrder: 0,
|
|
887
807
|
status: "finished",
|
|
808
|
+
format: "UIMessageChunk",
|
|
888
809
|
};
|
|
889
810
|
const abortedMsg: StreamMessage = {
|
|
890
811
|
streamId: "s3",
|
|
891
812
|
order: 2,
|
|
892
813
|
stepOrder: 0,
|
|
893
814
|
status: "aborted",
|
|
815
|
+
format: "UIMessageChunk",
|
|
894
816
|
};
|
|
895
817
|
|
|
896
818
|
const msgs = await deriveUIMessagesFromDeltas(
|
|
@@ -1001,10 +923,10 @@ describe("Stream Lifecycle Integration", () => {
|
|
|
1001
923
|
expect(finished).toHaveLength(1);
|
|
1002
924
|
|
|
1003
925
|
// 4. Derive UI messages from stored deltas
|
|
1004
|
-
const deltas = await ctx.runQuery(
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
);
|
|
926
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
927
|
+
threadId,
|
|
928
|
+
cursors: [{ cursor: 0, streamId }],
|
|
929
|
+
});
|
|
1008
930
|
const uiMessages = await deriveUIMessagesFromDeltas(
|
|
1009
931
|
threadId,
|
|
1010
932
|
finished,
|
|
@@ -1056,10 +978,10 @@ describe("Stream Lifecycle Integration", () => {
|
|
|
1056
978
|
expect(aborted[0].status).toBe("aborted");
|
|
1057
979
|
|
|
1058
980
|
// Even aborted streams have their deltas stored
|
|
1059
|
-
const deltas = await ctx.runQuery(
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
);
|
|
981
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
982
|
+
threadId,
|
|
983
|
+
cursors: [{ cursor: 0, streamId }],
|
|
984
|
+
});
|
|
1063
985
|
expect(deltas.length).toBeGreaterThan(0);
|
|
1064
986
|
});
|
|
1065
987
|
});
|
package/src/client/types.ts
CHANGED
|
@@ -32,8 +32,6 @@ export interface Output<_T = any, _P = any, _E = any> {
|
|
|
32
32
|
import type {
|
|
33
33
|
GenericActionCtx,
|
|
34
34
|
GenericDataModel,
|
|
35
|
-
GenericMutationCtx,
|
|
36
|
-
GenericQueryCtx,
|
|
37
35
|
WithoutSystemFields,
|
|
38
36
|
} from "convex/server";
|
|
39
37
|
import type {
|
|
@@ -356,10 +354,7 @@ export type TextArgs<
|
|
|
356
354
|
OUTPUT extends Output<any, any, any> = never,
|
|
357
355
|
> = Omit<
|
|
358
356
|
Parameters<
|
|
359
|
-
typeof generateText<
|
|
360
|
-
TOOLS extends undefined ? AgentTools : TOOLS,
|
|
361
|
-
OUTPUT
|
|
362
|
-
>
|
|
357
|
+
typeof generateText<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT>
|
|
363
358
|
>[0],
|
|
364
359
|
"model" | "prompt" | "messages"
|
|
365
360
|
> & {
|
|
@@ -376,10 +371,7 @@ export type StreamingTextArgs<
|
|
|
376
371
|
OUTPUT extends Output<any, any, any> = never,
|
|
377
372
|
> = Omit<
|
|
378
373
|
Parameters<
|
|
379
|
-
typeof streamText<
|
|
380
|
-
TOOLS extends undefined ? AgentTools : TOOLS,
|
|
381
|
-
OUTPUT
|
|
382
|
-
>
|
|
374
|
+
typeof streamText<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT>
|
|
383
375
|
>[0],
|
|
384
376
|
"model" | "prompt" | "messages"
|
|
385
377
|
> & {
|
|
@@ -496,11 +488,7 @@ export interface Thread<DefaultTools extends ToolSet> {
|
|
|
496
488
|
OUTPUT extends Output<any, any, any> = never,
|
|
497
489
|
>(
|
|
498
490
|
generateTextArgs: AgentPrompt &
|
|
499
|
-
TextArgs<
|
|
500
|
-
TOOLS extends undefined ? DefaultTools : TOOLS,
|
|
501
|
-
TOOLS,
|
|
502
|
-
OUTPUT
|
|
503
|
-
>,
|
|
491
|
+
TextArgs<TOOLS extends undefined ? DefaultTools : TOOLS, TOOLS, OUTPUT>,
|
|
504
492
|
options?: Options,
|
|
505
493
|
): Promise<
|
|
506
494
|
GenerateTextResult<TOOLS extends undefined ? DefaultTools : TOOLS, OUTPUT> &
|
|
@@ -541,10 +529,7 @@ export interface Thread<DefaultTools extends ToolSet> {
|
|
|
541
529
|
saveStreamDeltas?: boolean | StreamingOptions;
|
|
542
530
|
},
|
|
543
531
|
): Promise<
|
|
544
|
-
StreamTextResult<
|
|
545
|
-
TOOLS extends undefined ? DefaultTools : TOOLS,
|
|
546
|
-
OUTPUT
|
|
547
|
-
> &
|
|
532
|
+
StreamTextResult<TOOLS extends undefined ? DefaultTools : TOOLS, OUTPUT> &
|
|
548
533
|
ThreadOutputMetadata
|
|
549
534
|
>;
|
|
550
535
|
/**
|
|
@@ -629,9 +614,9 @@ export type SyncStreamsReturnValue =
|
|
|
629
614
|
| undefined;
|
|
630
615
|
|
|
631
616
|
/* Type utils follow */
|
|
632
|
-
export type QueryCtx = Pick<
|
|
617
|
+
export type QueryCtx = Pick<GenericActionCtx<GenericDataModel>, "runQuery">;
|
|
633
618
|
export type MutationCtx = Pick<
|
|
634
|
-
|
|
619
|
+
GenericActionCtx<GenericDataModel>,
|
|
635
620
|
"runQuery" | "runMutation"
|
|
636
621
|
>;
|
|
637
622
|
export type ActionCtx = Pick<
|
package/src/component/apiKeys.ts
CHANGED
|
@@ -13,7 +13,7 @@ export const issue = mutation({
|
|
|
13
13
|
.first();
|
|
14
14
|
if (existingApiKey) {
|
|
15
15
|
console.warn(`API key ${args.name} already exists, deleting...`);
|
|
16
|
-
await ctx.db.delete(existingApiKey._id);
|
|
16
|
+
await ctx.db.delete("apiKeys", existingApiKey._id);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
const apiKey = await ctx.db.insert("apiKeys", args);
|
|
@@ -27,7 +27,7 @@ export const validate = query({
|
|
|
27
27
|
apiKey: v.id("apiKeys"),
|
|
28
28
|
},
|
|
29
29
|
handler: async (ctx, args) => {
|
|
30
|
-
const apiKey = await ctx.db.get(args.apiKey);
|
|
30
|
+
const apiKey = await ctx.db.get("apiKeys", args.apiKey);
|
|
31
31
|
if (!apiKey) {
|
|
32
32
|
throw new Error("Invalid API key");
|
|
33
33
|
}
|
|
@@ -43,14 +43,14 @@ export const destroy = mutation({
|
|
|
43
43
|
}),
|
|
44
44
|
handler: async (ctx, args) => {
|
|
45
45
|
if (args.apiKey) {
|
|
46
|
-
const apiKey = await ctx.db.get(args.apiKey);
|
|
46
|
+
const apiKey = await ctx.db.get("apiKeys", args.apiKey);
|
|
47
47
|
if (!apiKey) {
|
|
48
48
|
return "missing";
|
|
49
49
|
}
|
|
50
50
|
if (apiKey.name !== args.name) {
|
|
51
51
|
return "name mismatch";
|
|
52
52
|
}
|
|
53
|
-
await ctx.db.delete(args.apiKey);
|
|
53
|
+
await ctx.db.delete("apiKeys", args.apiKey);
|
|
54
54
|
} else if (args.name) {
|
|
55
55
|
const apiKey = await ctx.db
|
|
56
56
|
.query("apiKeys")
|
|
@@ -59,7 +59,7 @@ export const destroy = mutation({
|
|
|
59
59
|
if (!apiKey) {
|
|
60
60
|
return "missing";
|
|
61
61
|
}
|
|
62
|
-
await ctx.db.delete(apiKey._id);
|
|
62
|
+
await ctx.db.delete("apiKeys", apiKey._id);
|
|
63
63
|
} else {
|
|
64
64
|
return "must provide either apiKey or name";
|
|
65
65
|
}
|