@convex-dev/agent 0.6.3 → 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.map +1 -1
- package/dist/component/apiKeys.js +5 -5
- package/dist/component/apiKeys.js.map +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 +2 -1
- 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 +4 -17
- 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
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
|
@@ -354,10 +354,7 @@ export type TextArgs<
|
|
|
354
354
|
OUTPUT extends Output<any, any, any> = never,
|
|
355
355
|
> = Omit<
|
|
356
356
|
Parameters<
|
|
357
|
-
typeof generateText<
|
|
358
|
-
TOOLS extends undefined ? AgentTools : TOOLS,
|
|
359
|
-
OUTPUT
|
|
360
|
-
>
|
|
357
|
+
typeof generateText<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT>
|
|
361
358
|
>[0],
|
|
362
359
|
"model" | "prompt" | "messages"
|
|
363
360
|
> & {
|
|
@@ -374,10 +371,7 @@ export type StreamingTextArgs<
|
|
|
374
371
|
OUTPUT extends Output<any, any, any> = never,
|
|
375
372
|
> = Omit<
|
|
376
373
|
Parameters<
|
|
377
|
-
typeof streamText<
|
|
378
|
-
TOOLS extends undefined ? AgentTools : TOOLS,
|
|
379
|
-
OUTPUT
|
|
380
|
-
>
|
|
374
|
+
typeof streamText<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT>
|
|
381
375
|
>[0],
|
|
382
376
|
"model" | "prompt" | "messages"
|
|
383
377
|
> & {
|
|
@@ -494,11 +488,7 @@ export interface Thread<DefaultTools extends ToolSet> {
|
|
|
494
488
|
OUTPUT extends Output<any, any, any> = never,
|
|
495
489
|
>(
|
|
496
490
|
generateTextArgs: AgentPrompt &
|
|
497
|
-
TextArgs<
|
|
498
|
-
TOOLS extends undefined ? DefaultTools : TOOLS,
|
|
499
|
-
TOOLS,
|
|
500
|
-
OUTPUT
|
|
501
|
-
>,
|
|
491
|
+
TextArgs<TOOLS extends undefined ? DefaultTools : TOOLS, TOOLS, OUTPUT>,
|
|
502
492
|
options?: Options,
|
|
503
493
|
): Promise<
|
|
504
494
|
GenerateTextResult<TOOLS extends undefined ? DefaultTools : TOOLS, OUTPUT> &
|
|
@@ -539,10 +529,7 @@ export interface Thread<DefaultTools extends ToolSet> {
|
|
|
539
529
|
saveStreamDeltas?: boolean | StreamingOptions;
|
|
540
530
|
},
|
|
541
531
|
): Promise<
|
|
542
|
-
StreamTextResult<
|
|
543
|
-
TOOLS extends undefined ? DefaultTools : TOOLS,
|
|
544
|
-
OUTPUT
|
|
545
|
-
> &
|
|
532
|
+
StreamTextResult<TOOLS extends undefined ? DefaultTools : TOOLS, OUTPUT> &
|
|
546
533
|
ThreadOutputMetadata
|
|
547
534
|
>;
|
|
548
535
|
/**
|
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
|
}
|
package/src/component/files.ts
CHANGED
|
@@ -29,15 +29,16 @@ export async function addFileHandler(
|
|
|
29
29
|
) {
|
|
30
30
|
// Support both mediaType (preferred) and mimeType (deprecated)
|
|
31
31
|
const mediaType = args.mediaType ?? args.mimeType;
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
const existingFile = await ctx.db
|
|
34
34
|
.query("files")
|
|
35
35
|
.withIndex("hash", (q) => q.eq("hash", args.hash))
|
|
36
|
+
// eslint-disable-next-line @convex-dev/no-filter-in-query -- We do not expect many files with the same hash and different filenames
|
|
36
37
|
.filter((q) => q.eq(q.field("filename"), args.filename))
|
|
37
38
|
.first();
|
|
38
39
|
if (existingFile) {
|
|
39
40
|
// increment the refcount
|
|
40
|
-
await ctx.db.patch(existingFile._id, {
|
|
41
|
+
await ctx.db.patch("files", existingFile._id, {
|
|
41
42
|
refcount: existingFile.refcount + 1,
|
|
42
43
|
lastTouchedAt: Date.now(),
|
|
43
44
|
});
|
|
@@ -68,7 +69,7 @@ export const get = query({
|
|
|
68
69
|
},
|
|
69
70
|
returns: v.union(v.null(), v.doc("files")),
|
|
70
71
|
handler: async (ctx, args) => {
|
|
71
|
-
return ctx.db.get(args.fileId);
|
|
72
|
+
return ctx.db.get("files", args.fileId);
|
|
72
73
|
},
|
|
73
74
|
});
|
|
74
75
|
|
|
@@ -87,12 +88,13 @@ export const useExistingFile = mutation({
|
|
|
87
88
|
const file = await ctx.db
|
|
88
89
|
.query("files")
|
|
89
90
|
.withIndex("hash", (q) => q.eq("hash", args.hash))
|
|
91
|
+
// eslint-disable-next-line @convex-dev/no-filter-in-query -- We do not expect many files with the same hash and different filenames
|
|
90
92
|
.filter((q) => q.eq(q.field("filename"), args.filename))
|
|
91
93
|
.first();
|
|
92
94
|
if (!file) {
|
|
93
95
|
return null;
|
|
94
96
|
}
|
|
95
|
-
await ctx.db.patch(file._id, {
|
|
97
|
+
await ctx.db.patch("files", file._id, {
|
|
96
98
|
lastTouchedAt: Date.now(),
|
|
97
99
|
});
|
|
98
100
|
return { fileId: file._id, storageId: file.storageId };
|
|
@@ -115,9 +117,9 @@ export async function changeRefcount(
|
|
|
115
117
|
const nextSet = new Set(next);
|
|
116
118
|
for (const fileId of prevSet) {
|
|
117
119
|
if (!nextSet.has(fileId)) {
|
|
118
|
-
const file = await ctx.db.get(fileId);
|
|
120
|
+
const file = await ctx.db.get("files", fileId);
|
|
119
121
|
if (file) {
|
|
120
|
-
await ctx.db.patch(fileId, {
|
|
122
|
+
await ctx.db.patch("files", fileId, {
|
|
121
123
|
refcount: file.refcount - 1,
|
|
122
124
|
});
|
|
123
125
|
} else {
|
|
@@ -127,9 +129,9 @@ export async function changeRefcount(
|
|
|
127
129
|
}
|
|
128
130
|
for (const fileId of nextSet) {
|
|
129
131
|
if (!prevSet.has(fileId)) {
|
|
130
|
-
const file = await ctx.db.get(fileId);
|
|
132
|
+
const file = await ctx.db.get("files", fileId);
|
|
131
133
|
if (file) {
|
|
132
|
-
await ctx.db.patch(fileId, {
|
|
134
|
+
await ctx.db.patch("files", fileId, {
|
|
133
135
|
refcount: file.refcount + 1,
|
|
134
136
|
});
|
|
135
137
|
} else {
|
|
@@ -151,11 +153,11 @@ export async function copyFileHandler(
|
|
|
151
153
|
ctx: MutationCtx,
|
|
152
154
|
args: { fileId: Id<"files"> },
|
|
153
155
|
) {
|
|
154
|
-
const file = await ctx.db.get(args.fileId);
|
|
156
|
+
const file = await ctx.db.get("files", args.fileId);
|
|
155
157
|
if (!file) {
|
|
156
158
|
throw new Error("File not found");
|
|
157
159
|
}
|
|
158
|
-
await ctx.db.patch(args.fileId, {
|
|
160
|
+
await ctx.db.patch("files", args.fileId, {
|
|
159
161
|
refcount: file.refcount + 1,
|
|
160
162
|
lastTouchedAt: Date.now(),
|
|
161
163
|
});
|
|
@@ -195,7 +197,7 @@ export const deleteFiles = mutation({
|
|
|
195
197
|
handler: async (ctx, args) => {
|
|
196
198
|
const deletedFileIds = await Promise.all(
|
|
197
199
|
args.fileIds.map(async (fileId) => {
|
|
198
|
-
const file = await ctx.db.get(fileId);
|
|
200
|
+
const file = await ctx.db.get("files", fileId);
|
|
199
201
|
if (!file) {
|
|
200
202
|
console.error(`File ${fileId} not found when deleting, skipping...`);
|
|
201
203
|
return null;
|
|
@@ -208,7 +210,7 @@ export const deleteFiles = mutation({
|
|
|
208
210
|
return null;
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
|
-
await ctx.db.delete(fileId);
|
|
213
|
+
await ctx.db.delete("files", fileId);
|
|
212
214
|
return fileId;
|
|
213
215
|
}),
|
|
214
216
|
);
|