@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.
- package/MIGRATION.md +153 -0
- package/README.md +32 -27
- package/dist/UIMessages.d.ts +46 -0
- package/dist/UIMessages.d.ts.map +1 -0
- package/dist/UIMessages.js +546 -0
- package/dist/UIMessages.js.map +1 -0
- package/dist/client/createTool.d.ts +126 -27
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +67 -12
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/defaultComponent.d.ts +11 -0
- package/dist/client/defaultComponent.d.ts.map +1 -0
- package/dist/client/defaultComponent.js +7 -0
- package/dist/client/defaultComponent.js.map +1 -0
- package/dist/client/definePlaygroundAPI.d.ts +1335 -204
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +52 -28
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/files.d.ts +20 -7
- package/dist/client/files.d.ts.map +1 -1
- package/dist/client/files.js +68 -11
- package/dist/client/files.js.map +1 -1
- package/dist/client/index.d.ts +1116 -978
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +332 -747
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +461 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +106 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/mockModel.d.ts +42 -0
- package/dist/client/mockModel.d.ts.map +1 -0
- package/dist/client/mockModel.js +182 -0
- package/dist/client/mockModel.js.map +1 -0
- package/dist/client/saveInputMessages.d.ts +20 -0
- package/dist/client/saveInputMessages.d.ts.map +1 -0
- package/dist/client/saveInputMessages.js +58 -0
- package/dist/client/saveInputMessages.js.map +1 -0
- package/dist/client/search.d.ts +350 -39
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +350 -39
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts +84 -0
- package/dist/client/start.d.ts.map +1 -0
- package/dist/client/start.js +185 -0
- package/dist/client/start.js.map +1 -0
- package/dist/client/streamText.d.ts +46 -0
- package/dist/client/streamText.d.ts.map +1 -0
- package/dist/client/streamText.js +117 -0
- package/dist/client/streamText.js.map +1 -0
- package/dist/client/streaming.d.ts +3716 -32
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +161 -59
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/threads.d.ts +46 -0
- package/dist/client/threads.d.ts.map +1 -0
- package/dist/client/threads.js +49 -0
- package/dist/client/threads.js.map +1 -0
- package/dist/client/types.d.ts +266 -128
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/utils.d.ts +4 -0
- package/dist/client/utils.d.ts.map +1 -0
- package/dist/client/utils.js +21 -0
- package/dist/client/utils.js.map +1 -0
- package/dist/component/_generated/api.d.ts +24 -2178
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js +10 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +3120 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +4 -18
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +10 -38
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +9 -5
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/files.d.ts +16 -10
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +10 -2
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts +2578 -366
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +397 -154
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +5697 -3584
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +18 -41
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +39 -339
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +114 -73
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts +13 -13
- package/dist/component/users.d.ts +7 -7
- package/dist/component/vector/index.d.ts +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +1 -3
- package/dist/component/vector/index.js.map +1 -1
- package/dist/deltas.d.ts +43 -0
- package/dist/deltas.d.ts.map +1 -0
- package/dist/deltas.js +446 -0
- package/dist/deltas.js.map +1 -0
- package/dist/mapping.d.ts +38 -20
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +365 -97
- package/dist/mapping.js.map +1 -1
- package/dist/react/SmoothText.d.ts +5 -0
- package/dist/react/SmoothText.d.ts.map +1 -0
- package/dist/react/SmoothText.js +6 -0
- package/dist/react/SmoothText.js.map +1 -0
- package/dist/react/index.d.ts +5 -77
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -160
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.d.ts +36 -3
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
- package/dist/react/optimisticallySendMessage.js +35 -9
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/react/types.d.ts +4 -18
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useDeltaStreams.d.ts +10 -0
- package/dist/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/react/useDeltaStreams.js +106 -0
- package/dist/react/useDeltaStreams.js.map +1 -0
- package/dist/react/useSmoothText.d.ts +13 -12
- package/dist/react/useSmoothText.d.ts.map +1 -1
- package/dist/react/useSmoothText.js +32 -15
- package/dist/react/useSmoothText.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +22 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/react/useStreamingUIMessages.js +92 -0
- package/dist/react/useStreamingUIMessages.js.map +1 -0
- package/dist/react/useThreadMessages.d.ts +104 -0
- package/dist/react/useThreadMessages.d.ts.map +1 -0
- package/dist/react/useThreadMessages.js +148 -0
- package/dist/react/useThreadMessages.js.map +1 -0
- package/dist/react/useUIMessages.d.ts +96 -0
- package/dist/react/useUIMessages.d.ts.map +1 -0
- package/dist/react/useUIMessages.js +108 -0
- package/dist/react/useUIMessages.js.map +1 -0
- package/dist/shared.d.ts +20 -4
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +45 -8
- package/dist/shared.js.map +1 -1
- package/dist/validators.d.ts +22981 -5666
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +245 -137
- package/dist/validators.js.map +1 -1
- package/package.json +101 -51
- package/src/UIMessages.combineUIMessages.test.ts +239 -0
- package/src/UIMessages.test.ts +273 -0
- package/src/UIMessages.ts +739 -0
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +291 -76
- package/src/client/defaultComponent.ts +17 -0
- package/src/client/definePlaygroundAPI.ts +67 -31
- package/src/client/files.ts +100 -20
- package/src/client/index.test.ts +40 -85
- package/src/client/index.ts +638 -1289
- package/src/client/messages.ts +237 -0
- package/src/client/mockModel.ts +252 -0
- package/src/client/saveInputMessages.test.ts +583 -0
- package/src/client/saveInputMessages.ts +101 -0
- package/src/client/search.test.ts +1207 -0
- package/src/client/search.ts +581 -70
- package/src/client/start.ts +327 -0
- package/src/client/streamText.ts +187 -0
- package/src/client/streaming.test.ts +186 -0
- package/src/client/streaming.ts +241 -97
- package/src/client/threads.ts +83 -0
- package/src/client/types.ts +370 -219
- package/src/client/utils.ts +27 -0
- package/src/component/_generated/api.ts +64 -0
- package/src/component/_generated/component.ts +4902 -0
- package/src/component/_generated/{server.d.ts → server.ts} +33 -21
- package/src/component/files.ts +11 -2
- package/src/component/messages.test.ts +195 -51
- package/src/component/messages.ts +500 -201
- package/src/component/schema.ts +20 -46
- package/src/component/setup.test.ts +7 -0
- package/src/component/streams.ts +184 -83
- package/src/component/users.test.ts +0 -1
- package/src/component/vector/index.ts +1 -3
- package/src/deltas.test.ts +626 -0
- package/src/deltas.ts +569 -0
- package/src/fromUIMessages.test.ts +497 -0
- package/src/mapping.test.ts +180 -6
- package/src/mapping.ts +479 -162
- package/src/react/SmoothText.tsx +9 -0
- package/src/react/index.ts +10 -230
- package/src/react/optimisticallySendMessage.ts +55 -12
- package/src/react/types.ts +6 -39
- package/src/react/useDeltaStreams.ts +160 -0
- package/src/react/useSmoothText.ts +56 -36
- package/src/react/useStreamingUIMessages.ts +143 -0
- package/src/react/useThreadMessages.ts +262 -0
- package/src/react/useUIMessages.test.ts +255 -0
- package/src/react/useUIMessages.ts +195 -0
- package/src/shared.ts +88 -12
- package/src/test.ts +18 -0
- package/src/toUIMessages.test.ts +1269 -0
- package/src/validators.test.ts +18 -19
- package/src/validators.ts +325 -185
- package/dist/client/_generated/_ignore.d.ts +0 -1
- package/dist/client/_generated/_ignore.d.ts.map +0 -1
- package/dist/client/_generated/_ignore.js +0 -3
- package/dist/client/_generated/_ignore.js.map +0 -1
- package/dist/client/listMessages.d.ts +0 -22
- package/dist/client/listMessages.d.ts.map +0 -1
- package/dist/client/listMessages.js +0 -25
- package/dist/client/listMessages.js.map +0 -1
- package/dist/package.json +0 -3
- package/dist/react/deltas.d.ts +0 -26
- package/dist/react/deltas.d.ts.map +0 -1
- package/dist/react/deltas.js +0 -384
- package/dist/react/deltas.js.map +0 -1
- package/dist/react/toUIMessages.d.ts +0 -15
- package/dist/react/toUIMessages.d.ts.map +0 -1
- package/dist/react/toUIMessages.js +0 -211
- package/dist/react/toUIMessages.js.map +0 -1
- package/src/client/listMessages.ts +0 -38
- package/src/component/_generated/api.d.ts +0 -2202
- package/src/component/_generated/api.js +0 -23
- package/src/component/_generated/server.js +0 -90
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/src/react/deltas.test.ts +0 -315
- package/src/react/deltas.ts +0 -478
- package/src/react/toUIMessages.test.ts +0 -420
- package/src/react/toUIMessages.ts +0 -253
- package/src/vitest.config.ts +0 -7
- /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { saveInputMessages } from "./saveInputMessages.js";
|
|
3
|
+
import type { MessageDoc } from "../validators.js";
|
|
4
|
+
import type { ActionCtx } from "./types.js";
|
|
5
|
+
import {
|
|
6
|
+
defineSchema,
|
|
7
|
+
type Auth,
|
|
8
|
+
type StorageActionWriter,
|
|
9
|
+
} from "convex/server";
|
|
10
|
+
import { initConvexTest } from "./setup.test.js";
|
|
11
|
+
import { components } from "./setup.test.js";
|
|
12
|
+
|
|
13
|
+
const schema = defineSchema({});
|
|
14
|
+
|
|
15
|
+
// Hoisted mock functions
|
|
16
|
+
const { mockSaveMessages, mockEmbedMessages } = vi.hoisted(() => ({
|
|
17
|
+
mockSaveMessages: vi.fn(),
|
|
18
|
+
mockEmbedMessages: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("./messages.js", () => ({
|
|
22
|
+
saveMessages: mockSaveMessages,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("./search.js", async () => {
|
|
26
|
+
const actual = await vi.importActual("./search.js");
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
embedMessages: mockEmbedMessages,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Helper to create mock MessageDoc
|
|
34
|
+
const createMockMessageDoc = (
|
|
35
|
+
id: string,
|
|
36
|
+
role: "user" | "assistant" | "tool" | "system",
|
|
37
|
+
content: any,
|
|
38
|
+
): MessageDoc => ({
|
|
39
|
+
_id: id,
|
|
40
|
+
_creationTime: Date.now(),
|
|
41
|
+
userId: "test-user",
|
|
42
|
+
threadId: "test-thread",
|
|
43
|
+
order: 1,
|
|
44
|
+
stepOrder: 1,
|
|
45
|
+
status: role === "assistant" ? "pending" : "success",
|
|
46
|
+
tool: false,
|
|
47
|
+
message: { role, content },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("saveInputMessages", () => {
|
|
51
|
+
const defaultArgs = {
|
|
52
|
+
threadId: "test-thread",
|
|
53
|
+
userId: "test-user",
|
|
54
|
+
promptMessageId: undefined,
|
|
55
|
+
agentName: "test-agent",
|
|
56
|
+
storageOptions: { saveMessages: "promptAndOutput" as const },
|
|
57
|
+
usageHandler: undefined,
|
|
58
|
+
textEmbeddingModel: undefined,
|
|
59
|
+
callSettings: undefined,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const mockComponent = components.agent;
|
|
63
|
+
|
|
64
|
+
let t = initConvexTest(schema);
|
|
65
|
+
let ctx: ActionCtx;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
vi.clearAllMocks();
|
|
69
|
+
t = initConvexTest(schema);
|
|
70
|
+
ctx = {
|
|
71
|
+
runQuery: t.query,
|
|
72
|
+
runAction: t.action,
|
|
73
|
+
runMutation: t.mutation,
|
|
74
|
+
auth: {} as Auth,
|
|
75
|
+
storage: {} as StorageActionWriter,
|
|
76
|
+
} as ActionCtx;
|
|
77
|
+
|
|
78
|
+
mockSaveMessages.mockResolvedValue({
|
|
79
|
+
messages: [
|
|
80
|
+
createMockMessageDoc("saved-1", "user", "Test prompt"),
|
|
81
|
+
createMockMessageDoc("pending-1", "assistant", []),
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
mockEmbedMessages.mockResolvedValue({
|
|
86
|
+
vectors: [[0.1, 0.2, 0.3], null],
|
|
87
|
+
dimension: 3,
|
|
88
|
+
model: "test-model",
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("saveMessages: 'all' scenarios", () => {
|
|
93
|
+
test("should save all messages and prompt when storageOptions.saveMessages is 'all'", async () => {
|
|
94
|
+
const t = initConvexTest(schema);
|
|
95
|
+
|
|
96
|
+
await t.run(async (ctx) => {
|
|
97
|
+
const prompt = "Test prompt";
|
|
98
|
+
const messages = [
|
|
99
|
+
{ role: "user" as const, content: "Previous message 1" },
|
|
100
|
+
{ role: "assistant" as const, content: "Response 1" },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const result = await saveInputMessages(ctx, mockComponent, {
|
|
104
|
+
...defaultArgs,
|
|
105
|
+
prompt,
|
|
106
|
+
messages,
|
|
107
|
+
storageOptions: { saveMessages: "all" },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
111
|
+
ctx,
|
|
112
|
+
mockComponent,
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
threadId: "test-thread",
|
|
115
|
+
userId: "test-user",
|
|
116
|
+
messages: [
|
|
117
|
+
...messages,
|
|
118
|
+
{ role: "user", content: "Test prompt" },
|
|
119
|
+
{ role: "assistant", content: [] },
|
|
120
|
+
],
|
|
121
|
+
metadata: expect.arrayContaining([
|
|
122
|
+
{},
|
|
123
|
+
{},
|
|
124
|
+
{},
|
|
125
|
+
{ status: "pending" },
|
|
126
|
+
]),
|
|
127
|
+
failPendingSteps: false,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result.promptMessageId).toBe("saved-1");
|
|
132
|
+
expect(result.pendingMessage?._id).toBe("pending-1");
|
|
133
|
+
expect(result.savedMessages).toHaveLength(1);
|
|
134
|
+
expect(result.savedMessages[0]._id).toBe("saved-1");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("should save all with promptMessageId provided (no new messages saved)", async () => {
|
|
139
|
+
const t = initConvexTest(schema);
|
|
140
|
+
|
|
141
|
+
await t.run(async (ctx) => {
|
|
142
|
+
// Mock saveMessages to return only pending message
|
|
143
|
+
mockSaveMessages.mockResolvedValueOnce({
|
|
144
|
+
messages: [createMockMessageDoc("pending-1", "assistant", [])],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const prompt = "Test prompt";
|
|
148
|
+
const messages = [
|
|
149
|
+
{ role: "user" as const, content: "Previous message" },
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const result = await saveInputMessages(ctx, mockComponent, {
|
|
153
|
+
...defaultArgs,
|
|
154
|
+
prompt,
|
|
155
|
+
messages,
|
|
156
|
+
promptMessageId: "existing-prompt-id",
|
|
157
|
+
storageOptions: { saveMessages: "all" },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Should not save any input messages when promptMessageId is provided
|
|
161
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
162
|
+
ctx,
|
|
163
|
+
mockComponent,
|
|
164
|
+
expect.objectContaining({
|
|
165
|
+
messages: [{ role: "assistant", content: [] }],
|
|
166
|
+
metadata: [{ status: "pending" }],
|
|
167
|
+
failPendingSteps: true,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(result.promptMessageId).toBe("existing-prompt-id");
|
|
172
|
+
expect(result.savedMessages).toHaveLength(0);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("should save all with only prompt messages provided", async () => {
|
|
177
|
+
const t = initConvexTest(schema);
|
|
178
|
+
|
|
179
|
+
await t.run(async (ctx) => {
|
|
180
|
+
const prompt = [
|
|
181
|
+
{ role: "user" as const, content: "Multi-part prompt 1" },
|
|
182
|
+
{ role: "user" as const, content: "Multi-part prompt 2" },
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const result = await saveInputMessages(ctx, mockComponent, {
|
|
186
|
+
...defaultArgs,
|
|
187
|
+
prompt,
|
|
188
|
+
messages: undefined,
|
|
189
|
+
storageOptions: { saveMessages: "all" },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
193
|
+
ctx,
|
|
194
|
+
mockComponent,
|
|
195
|
+
expect.objectContaining({
|
|
196
|
+
messages: [...prompt, { role: "assistant", content: [] }],
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(result.savedMessages).toHaveLength(1);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("should save all with both prompt and messages provided", async () => {
|
|
205
|
+
const t = initConvexTest(schema);
|
|
206
|
+
|
|
207
|
+
await t.run(async (ctx) => {
|
|
208
|
+
const prompt = "Single prompt";
|
|
209
|
+
const messages = [
|
|
210
|
+
{ role: "user" as const, content: "Context message" },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
await saveInputMessages(ctx, mockComponent, {
|
|
214
|
+
...defaultArgs,
|
|
215
|
+
prompt,
|
|
216
|
+
messages,
|
|
217
|
+
storageOptions: { saveMessages: "all" },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
221
|
+
ctx,
|
|
222
|
+
mockComponent,
|
|
223
|
+
expect.objectContaining({
|
|
224
|
+
messages: [
|
|
225
|
+
...messages,
|
|
226
|
+
{ role: "user", content: "Single prompt" },
|
|
227
|
+
{ role: "assistant", content: [] },
|
|
228
|
+
],
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("saveMessages: 'promptAndOutput' scenarios", () => {
|
|
236
|
+
test("should save only prompt when storageOptions.saveMessages is 'promptAndOutput'", async () => {
|
|
237
|
+
const t = initConvexTest(schema);
|
|
238
|
+
|
|
239
|
+
await t.run(async (ctx) => {
|
|
240
|
+
const prompt = "Test prompt";
|
|
241
|
+
const messages = [
|
|
242
|
+
{ role: "user" as const, content: "Previous message" },
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
const result = await saveInputMessages(ctx, mockComponent, {
|
|
246
|
+
...defaultArgs,
|
|
247
|
+
prompt,
|
|
248
|
+
messages,
|
|
249
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
253
|
+
ctx,
|
|
254
|
+
mockComponent,
|
|
255
|
+
expect.objectContaining({
|
|
256
|
+
messages: [
|
|
257
|
+
{ role: "user", content: "Test prompt" },
|
|
258
|
+
{ role: "assistant", content: [] },
|
|
259
|
+
],
|
|
260
|
+
metadata: [{}, { status: "pending" }],
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(result.promptMessageId).toBe("saved-1");
|
|
265
|
+
expect(result.savedMessages).toHaveLength(1);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("should save prompt array when provided with promptAndOutput", async () => {
|
|
270
|
+
const t = initConvexTest(schema);
|
|
271
|
+
|
|
272
|
+
await t.run(async (ctx) => {
|
|
273
|
+
const prompt = [
|
|
274
|
+
{ role: "user" as const, content: "Part 1" },
|
|
275
|
+
{ role: "user" as const, content: "Part 2" },
|
|
276
|
+
];
|
|
277
|
+
const messages = [
|
|
278
|
+
{ role: "user" as const, content: "Context message" },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
await saveInputMessages(ctx, mockComponent, {
|
|
282
|
+
...defaultArgs,
|
|
283
|
+
prompt,
|
|
284
|
+
messages,
|
|
285
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
289
|
+
ctx,
|
|
290
|
+
mockComponent,
|
|
291
|
+
expect.objectContaining({
|
|
292
|
+
messages: [...prompt, { role: "assistant", content: [] }],
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("should save last message when no prompt provided with promptAndOutput", async () => {
|
|
299
|
+
const t = initConvexTest(schema);
|
|
300
|
+
|
|
301
|
+
await t.run(async (ctx) => {
|
|
302
|
+
const messages = [
|
|
303
|
+
{ role: "user" as const, content: "First message" },
|
|
304
|
+
{ role: "user" as const, content: "Last message" },
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
await saveInputMessages(ctx, mockComponent, {
|
|
308
|
+
...defaultArgs,
|
|
309
|
+
prompt: undefined,
|
|
310
|
+
messages,
|
|
311
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
315
|
+
ctx,
|
|
316
|
+
mockComponent,
|
|
317
|
+
expect.objectContaining({
|
|
318
|
+
messages: [
|
|
319
|
+
{ role: "user", content: "Last message" },
|
|
320
|
+
{ role: "assistant", content: [] },
|
|
321
|
+
],
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("should handle promptMessageId with promptAndOutput (no new messages saved)", async () => {
|
|
328
|
+
const t = initConvexTest(schema);
|
|
329
|
+
|
|
330
|
+
await t.run(async (ctx) => {
|
|
331
|
+
// Mock saveMessages to return only pending message
|
|
332
|
+
mockSaveMessages.mockResolvedValueOnce({
|
|
333
|
+
messages: [createMockMessageDoc("pending-1", "assistant", [])],
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const result = await saveInputMessages(ctx, mockComponent, {
|
|
337
|
+
...defaultArgs,
|
|
338
|
+
prompt: "Test prompt",
|
|
339
|
+
messages: [{ role: "user" as const, content: "Context" }],
|
|
340
|
+
promptMessageId: "existing-prompt-id",
|
|
341
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
345
|
+
ctx,
|
|
346
|
+
mockComponent,
|
|
347
|
+
expect.objectContaining({
|
|
348
|
+
messages: [{ role: "assistant", content: [] }],
|
|
349
|
+
failPendingSteps: true,
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
expect(result.promptMessageId).toBe("existing-prompt-id");
|
|
354
|
+
expect(result.savedMessages).toHaveLength(0);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("embedding generation scenarios", () => {
|
|
360
|
+
test("should generate embeddings when textEmbeddingModel is provided (action context)", async () => {
|
|
361
|
+
// Create action context with runAction method
|
|
362
|
+
const actionCtx = {
|
|
363
|
+
runQuery: vi.fn(),
|
|
364
|
+
runMutation: vi.fn(),
|
|
365
|
+
runAction: vi.fn(),
|
|
366
|
+
auth: {} as Auth,
|
|
367
|
+
storage: {} as StorageActionWriter,
|
|
368
|
+
} as ActionCtx;
|
|
369
|
+
|
|
370
|
+
await saveInputMessages(actionCtx, mockComponent, {
|
|
371
|
+
...defaultArgs,
|
|
372
|
+
prompt: "Test prompt",
|
|
373
|
+
messages: undefined,
|
|
374
|
+
textEmbeddingModel: "test-embedding-model",
|
|
375
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Verify embedMessages was called
|
|
379
|
+
expect(mockEmbedMessages).toHaveBeenCalledWith(
|
|
380
|
+
expect.anything(),
|
|
381
|
+
expect.objectContaining({
|
|
382
|
+
userId: "test-user",
|
|
383
|
+
threadId: "test-thread",
|
|
384
|
+
textEmbeddingModel: "test-embedding-model",
|
|
385
|
+
}),
|
|
386
|
+
[{ role: "user", content: "Test prompt" }],
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Verify saveMessages was called with embeddings
|
|
390
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
391
|
+
expect.anything(),
|
|
392
|
+
expect.anything(),
|
|
393
|
+
expect.objectContaining({
|
|
394
|
+
embeddings: expect.objectContaining({
|
|
395
|
+
vectors: expect.arrayContaining([[0.1, 0.2, 0.3], null]),
|
|
396
|
+
dimension: 3,
|
|
397
|
+
model: "test-model",
|
|
398
|
+
}),
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("should not generate embeddings in mutation context even with textEmbeddingModel", async () => {
|
|
404
|
+
await expect(
|
|
405
|
+
t.run(async (ctx) => {
|
|
406
|
+
return saveInputMessages(ctx, mockComponent, {
|
|
407
|
+
...defaultArgs,
|
|
408
|
+
prompt: "Test prompt",
|
|
409
|
+
messages: undefined,
|
|
410
|
+
textEmbeddingModel: "test-embedding-model",
|
|
411
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
412
|
+
});
|
|
413
|
+
}),
|
|
414
|
+
).rejects.toThrow(
|
|
415
|
+
"You must be in an action context to generate embeddings",
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("should not generate embeddings when no textEmbeddingModel provided", async () => {
|
|
420
|
+
await saveInputMessages(ctx, mockComponent, {
|
|
421
|
+
...defaultArgs,
|
|
422
|
+
prompt: "Test prompt",
|
|
423
|
+
messages: undefined,
|
|
424
|
+
textEmbeddingModel: undefined,
|
|
425
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(mockEmbedMessages).not.toHaveBeenCalled();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("should not generate embeddings when no messages to save", async () => {
|
|
432
|
+
await saveInputMessages(ctx, mockComponent, {
|
|
433
|
+
...defaultArgs,
|
|
434
|
+
prompt: undefined,
|
|
435
|
+
messages: undefined,
|
|
436
|
+
promptMessageId: "existing-id",
|
|
437
|
+
textEmbeddingModel: "test-model",
|
|
438
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
expect(mockEmbedMessages).not.toHaveBeenCalled();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe("edge cases and validation", () => {
|
|
446
|
+
test("should handle empty prompt and messages gracefully", async () => {
|
|
447
|
+
const t = initConvexTest(schema);
|
|
448
|
+
|
|
449
|
+
await t.run(async (ctx) => {
|
|
450
|
+
// Mock saveMessages to return only pending message
|
|
451
|
+
mockSaveMessages.mockResolvedValueOnce({
|
|
452
|
+
messages: [createMockMessageDoc("pending-1", "assistant", [])],
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const result = await saveInputMessages(ctx, mockComponent, {
|
|
456
|
+
...defaultArgs,
|
|
457
|
+
prompt: undefined,
|
|
458
|
+
messages: undefined,
|
|
459
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
463
|
+
ctx,
|
|
464
|
+
mockComponent,
|
|
465
|
+
expect.objectContaining({
|
|
466
|
+
messages: [{ role: "assistant", content: [] }],
|
|
467
|
+
metadata: [{ status: "pending" }],
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
expect(result.promptMessageId).toBeUndefined();
|
|
472
|
+
expect(result.savedMessages).toHaveLength(0);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("should default to 'promptAndOutput' when storageOptions.saveMessages is not specified", async () => {
|
|
477
|
+
const t = initConvexTest(schema);
|
|
478
|
+
|
|
479
|
+
await t.run(async (ctx) => {
|
|
480
|
+
const prompt = "Test prompt";
|
|
481
|
+
const messages = [
|
|
482
|
+
{ role: "user" as const, content: "Context message" },
|
|
483
|
+
];
|
|
484
|
+
|
|
485
|
+
await saveInputMessages(ctx, mockComponent, {
|
|
486
|
+
...defaultArgs,
|
|
487
|
+
prompt,
|
|
488
|
+
messages,
|
|
489
|
+
storageOptions: undefined,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Should behave like promptAndOutput - only save prompt
|
|
493
|
+
expect(mockSaveMessages).toHaveBeenCalledWith(
|
|
494
|
+
ctx,
|
|
495
|
+
mockComponent,
|
|
496
|
+
expect.objectContaining({
|
|
497
|
+
messages: [
|
|
498
|
+
{ role: "user", content: "Test prompt" },
|
|
499
|
+
{ role: "assistant", content: [] },
|
|
500
|
+
],
|
|
501
|
+
}),
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("should always include pending message in saved messages", async () => {
|
|
507
|
+
const t = initConvexTest(schema);
|
|
508
|
+
|
|
509
|
+
await t.run(async (ctx) => {
|
|
510
|
+
const result = await saveInputMessages(ctx, mockComponent, {
|
|
511
|
+
...defaultArgs,
|
|
512
|
+
prompt: "Test prompt",
|
|
513
|
+
messages: undefined,
|
|
514
|
+
storageOptions: { saveMessages: "all" },
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(result.pendingMessage.status).toBe("pending");
|
|
518
|
+
expect(result.pendingMessage.message?.role).toBe("assistant");
|
|
519
|
+
expect(result.pendingMessage.message?.content).toEqual([]);
|
|
520
|
+
|
|
521
|
+
// Pending message should NOT be included in savedMessages
|
|
522
|
+
expect(result.savedMessages).not.toContainEqual(
|
|
523
|
+
expect.objectContaining({ status: "pending" }),
|
|
524
|
+
);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test("should return correct promptMessageId when messages are saved", async () => {
|
|
529
|
+
const t = initConvexTest(schema);
|
|
530
|
+
|
|
531
|
+
await t.run(async (ctx) => {
|
|
532
|
+
// Mock saveMessages to return multiple messages
|
|
533
|
+
mockSaveMessages.mockResolvedValueOnce({
|
|
534
|
+
messages: [
|
|
535
|
+
createMockMessageDoc("msg-1", "user", "First"),
|
|
536
|
+
createMockMessageDoc(
|
|
537
|
+
"msg-2",
|
|
538
|
+
"user",
|
|
539
|
+
"Second - this should be the prompt",
|
|
540
|
+
),
|
|
541
|
+
createMockMessageDoc("pending-1", "assistant", []),
|
|
542
|
+
],
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const result = await saveInputMessages(ctx, mockComponent, {
|
|
546
|
+
...defaultArgs,
|
|
547
|
+
prompt: "Test prompt",
|
|
548
|
+
messages: [{ role: "user" as const, content: "Context" }],
|
|
549
|
+
storageOptions: { saveMessages: "all" },
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// promptMessageId should be the second-to-last message (before pending)
|
|
553
|
+
expect(result.promptMessageId).toBe("msg-2");
|
|
554
|
+
expect(result.savedMessages).toHaveLength(2);
|
|
555
|
+
expect(result.savedMessages.map((m) => m._id)).toEqual([
|
|
556
|
+
"msg-1",
|
|
557
|
+
"msg-2",
|
|
558
|
+
]);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("should use provided promptMessageId when no new messages are saved", async () => {
|
|
563
|
+
const t = initConvexTest(schema);
|
|
564
|
+
|
|
565
|
+
await t.run(async (ctx) => {
|
|
566
|
+
mockSaveMessages.mockResolvedValueOnce({
|
|
567
|
+
messages: [createMockMessageDoc("pending-1", "assistant", [])],
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const result = await saveInputMessages(ctx, mockComponent, {
|
|
571
|
+
...defaultArgs,
|
|
572
|
+
prompt: "Test prompt",
|
|
573
|
+
messages: undefined,
|
|
574
|
+
promptMessageId: "existing-prompt-123",
|
|
575
|
+
storageOptions: { saveMessages: "promptAndOutput" },
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
expect(result.promptMessageId).toBe("existing-prompt-123");
|
|
579
|
+
expect(result.savedMessages).toHaveLength(0);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type ModelMessage } from "ai";
|
|
2
|
+
import type { MessageDoc } from "../validators.js";
|
|
3
|
+
import { embedMessages, getPromptArray } from "./search.js";
|
|
4
|
+
import type {
|
|
5
|
+
ActionCtx,
|
|
6
|
+
AgentComponent,
|
|
7
|
+
Config,
|
|
8
|
+
MutationCtx,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
import { saveMessages } from "./messages.js";
|
|
11
|
+
import type { Message } from "../validators.js";
|
|
12
|
+
import { assert } from "convex-helpers";
|
|
13
|
+
import type { VectorDimension } from "../component/vector/tables.js";
|
|
14
|
+
|
|
15
|
+
export async function saveInputMessages(
|
|
16
|
+
ctx: MutationCtx | ActionCtx,
|
|
17
|
+
component: AgentComponent,
|
|
18
|
+
{
|
|
19
|
+
threadId,
|
|
20
|
+
userId,
|
|
21
|
+
prompt,
|
|
22
|
+
messages,
|
|
23
|
+
...args
|
|
24
|
+
}: {
|
|
25
|
+
prompt: string | (ModelMessage | Message)[] | undefined;
|
|
26
|
+
messages: (ModelMessage | Message)[] | undefined;
|
|
27
|
+
promptMessageId: string | undefined;
|
|
28
|
+
userId: string | undefined;
|
|
29
|
+
threadId: string;
|
|
30
|
+
agentName?: string;
|
|
31
|
+
storageOptions?: {
|
|
32
|
+
saveMessages?: "all" | "promptAndOutput";
|
|
33
|
+
};
|
|
34
|
+
} & Pick<Config, "usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings">,
|
|
35
|
+
): Promise<{
|
|
36
|
+
promptMessageId: string | undefined;
|
|
37
|
+
pendingMessage: MessageDoc;
|
|
38
|
+
savedMessages: MessageDoc[];
|
|
39
|
+
}> {
|
|
40
|
+
const shouldSave = args.storageOptions?.saveMessages ?? "promptAndOutput";
|
|
41
|
+
// If only a promptMessageId is provided, this will be empty.
|
|
42
|
+
const promptArray = getPromptArray(prompt);
|
|
43
|
+
|
|
44
|
+
const toSave: (ModelMessage | Message)[] = [];
|
|
45
|
+
if (args.promptMessageId) {
|
|
46
|
+
// We don't save any inputs if a promptMessageId is provided.
|
|
47
|
+
// It's unclear where they'd want to save the new messages.
|
|
48
|
+
} else if (shouldSave === "all") {
|
|
49
|
+
if (messages) toSave.push(...messages);
|
|
50
|
+
toSave.push(...promptArray);
|
|
51
|
+
} else {
|
|
52
|
+
if (promptArray.length) {
|
|
53
|
+
// We treat the whole promptArray as the prompt message to save.
|
|
54
|
+
toSave.push(...promptArray);
|
|
55
|
+
} else if (messages) {
|
|
56
|
+
// Otherwise, treat the last message as the prompt message to save.
|
|
57
|
+
toSave.push(...messages.slice(-1));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let embeddings:
|
|
61
|
+
| {
|
|
62
|
+
vectors: (number[] | null)[];
|
|
63
|
+
dimension: VectorDimension;
|
|
64
|
+
model: string;
|
|
65
|
+
}
|
|
66
|
+
| undefined;
|
|
67
|
+
if ((args.embeddingModel ?? args.textEmbeddingModel) && toSave.length) {
|
|
68
|
+
assert(
|
|
69
|
+
"runAction" in ctx,
|
|
70
|
+
"You must be in an action context to generate embeddings",
|
|
71
|
+
);
|
|
72
|
+
embeddings = await embedMessages(
|
|
73
|
+
ctx,
|
|
74
|
+
{ ...args, userId: userId ?? undefined, threadId },
|
|
75
|
+
toSave,
|
|
76
|
+
);
|
|
77
|
+
if (embeddings) {
|
|
78
|
+
// for the pending message
|
|
79
|
+
embeddings.vectors.push(null);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const saved = await saveMessages(ctx, component, {
|
|
83
|
+
threadId,
|
|
84
|
+
userId,
|
|
85
|
+
messages: [...toSave, { role: "assistant", content: [] }],
|
|
86
|
+
metadata: [
|
|
87
|
+
...Array.from({ length: toSave.length }, () => ({})),
|
|
88
|
+
{ status: "pending" },
|
|
89
|
+
],
|
|
90
|
+
failPendingSteps: !!args.promptMessageId,
|
|
91
|
+
promptMessageId: args.promptMessageId,
|
|
92
|
+
embeddings,
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
promptMessageId: toSave.length
|
|
96
|
+
? saved.messages.at(-2)!._id
|
|
97
|
+
: args.promptMessageId,
|
|
98
|
+
pendingMessage: saved.messages.at(-1)!,
|
|
99
|
+
savedMessages: saved.messages.slice(0, -1),
|
|
100
|
+
};
|
|
101
|
+
}
|