@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,1207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
it,
|
|
4
|
+
expect,
|
|
5
|
+
vi,
|
|
6
|
+
beforeEach,
|
|
7
|
+
type MockedFunction,
|
|
8
|
+
} from "vitest";
|
|
9
|
+
import type { ModelMessage } from "ai";
|
|
10
|
+
import {
|
|
11
|
+
defineSchema,
|
|
12
|
+
type Auth,
|
|
13
|
+
type StorageActionWriter,
|
|
14
|
+
type StorageReader,
|
|
15
|
+
} from "convex/server";
|
|
16
|
+
import type { MessageDoc } from "../validators.js";
|
|
17
|
+
import type { ActionCtx, QueryCtx } from "./types.js";
|
|
18
|
+
import {
|
|
19
|
+
fetchContextWithPrompt,
|
|
20
|
+
fetchContextMessages,
|
|
21
|
+
filterOutOrphanedToolMessages,
|
|
22
|
+
getPromptArray,
|
|
23
|
+
} from "./search.js";
|
|
24
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
25
|
+
import { createThread } from "./threads.js";
|
|
26
|
+
import { saveMessages } from "./messages.js";
|
|
27
|
+
|
|
28
|
+
// Helper to create mock MessageDoc
|
|
29
|
+
const createMockMessageDoc = (
|
|
30
|
+
id: string,
|
|
31
|
+
role: "user" | "assistant" | "tool" | "system",
|
|
32
|
+
content: any,
|
|
33
|
+
order: number = 1,
|
|
34
|
+
): MessageDoc => ({
|
|
35
|
+
_id: id,
|
|
36
|
+
_creationTime: Date.now(),
|
|
37
|
+
userId: "test-user",
|
|
38
|
+
threadId: "test-thread",
|
|
39
|
+
order,
|
|
40
|
+
stepOrder: order,
|
|
41
|
+
status: "success",
|
|
42
|
+
tool: false,
|
|
43
|
+
message: { role, content },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const schema = defineSchema({});
|
|
47
|
+
|
|
48
|
+
describe("search.ts", () => {
|
|
49
|
+
let t = initConvexTest(schema);
|
|
50
|
+
let mockCtx: ActionCtx;
|
|
51
|
+
let ctx: ActionCtx;
|
|
52
|
+
|
|
53
|
+
// Shared helper functions
|
|
54
|
+
async function createTestThread(userId: string) {
|
|
55
|
+
return await t.run(async (mutCtx) => {
|
|
56
|
+
return await createThread(mutCtx, components.agent, {
|
|
57
|
+
userId,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function createTestMessages(
|
|
63
|
+
threadId: string,
|
|
64
|
+
userId: string,
|
|
65
|
+
messages: Array<{
|
|
66
|
+
role: "user" | "assistant";
|
|
67
|
+
content: string;
|
|
68
|
+
order: number;
|
|
69
|
+
}>,
|
|
70
|
+
) {
|
|
71
|
+
await t.run(async (mutCtx) => {
|
|
72
|
+
await saveMessages(mutCtx, components.agent, {
|
|
73
|
+
threadId,
|
|
74
|
+
userId,
|
|
75
|
+
messages: messages.map((msg) => ({
|
|
76
|
+
role: msg.role,
|
|
77
|
+
content: msg.content,
|
|
78
|
+
})),
|
|
79
|
+
metadata: messages.map((msg) => ({
|
|
80
|
+
order: msg.order,
|
|
81
|
+
stepOrder: msg.order,
|
|
82
|
+
status: "success" as const,
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
t = initConvexTest(schema);
|
|
91
|
+
ctx = {
|
|
92
|
+
runQuery: t.query,
|
|
93
|
+
runAction: t.action,
|
|
94
|
+
runMutation: t.mutation,
|
|
95
|
+
} as ActionCtx;
|
|
96
|
+
|
|
97
|
+
mockCtx = {
|
|
98
|
+
runQuery: vi.fn(),
|
|
99
|
+
runAction: vi.fn(),
|
|
100
|
+
runMutation: vi.fn(),
|
|
101
|
+
auth: {} as Auth,
|
|
102
|
+
storage: {} as StorageActionWriter,
|
|
103
|
+
} satisfies ActionCtx;
|
|
104
|
+
|
|
105
|
+
// Mock process.env to avoid file inlining in tests
|
|
106
|
+
process.env.CONVEX_CLOUD_URL = "https://example.convex.cloud";
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("getPromptArray", () => {
|
|
110
|
+
it("should return empty array for undefined prompt", () => {
|
|
111
|
+
expect(getPromptArray(undefined)).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should return array as-is for array prompt", () => {
|
|
115
|
+
const prompt: ModelMessage[] = [
|
|
116
|
+
{ role: "user", content: "Hello" },
|
|
117
|
+
{ role: "assistant", content: "Hi there!" },
|
|
118
|
+
];
|
|
119
|
+
expect(getPromptArray(prompt)).toEqual(prompt);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should convert string prompt to user message", () => {
|
|
123
|
+
const prompt = "Hello world";
|
|
124
|
+
expect(getPromptArray(prompt)).toEqual([
|
|
125
|
+
{ role: "user", content: "Hello world" },
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("filterOutOrphanedToolMessages", () => {
|
|
131
|
+
it("should keep non-tool messages", () => {
|
|
132
|
+
const messages: MessageDoc[] = [
|
|
133
|
+
{
|
|
134
|
+
_id: "1",
|
|
135
|
+
message: { role: "user", content: "Hello" },
|
|
136
|
+
order: 1,
|
|
137
|
+
} as MessageDoc,
|
|
138
|
+
{
|
|
139
|
+
_id: "2",
|
|
140
|
+
message: { role: "assistant", content: "Hi!" },
|
|
141
|
+
order: 2,
|
|
142
|
+
} as MessageDoc,
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const result = filterOutOrphanedToolMessages(messages);
|
|
146
|
+
expect(result).toHaveLength(2);
|
|
147
|
+
expect(result).toEqual(messages);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should keep tool messages with corresponding tool calls", () => {
|
|
151
|
+
const messages: MessageDoc[] = [
|
|
152
|
+
{
|
|
153
|
+
_id: "1",
|
|
154
|
+
message: {
|
|
155
|
+
role: "assistant",
|
|
156
|
+
content: [
|
|
157
|
+
{ type: "text", text: "I'll help you with that" },
|
|
158
|
+
{
|
|
159
|
+
type: "tool-call",
|
|
160
|
+
toolCallId: "call_123",
|
|
161
|
+
toolName: "test",
|
|
162
|
+
input: {},
|
|
163
|
+
args: {},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
order: 1,
|
|
168
|
+
} as MessageDoc,
|
|
169
|
+
{
|
|
170
|
+
_id: "2",
|
|
171
|
+
message: {
|
|
172
|
+
role: "tool",
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "tool-result",
|
|
176
|
+
toolCallId: "call_123",
|
|
177
|
+
result: "success",
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
order: 2,
|
|
182
|
+
} as MessageDoc,
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const result = filterOutOrphanedToolMessages(messages);
|
|
186
|
+
expect(result).toHaveLength(2);
|
|
187
|
+
expect(result).toEqual(messages);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should filter out orphaned tool messages", () => {
|
|
191
|
+
const messages: MessageDoc[] = [
|
|
192
|
+
{
|
|
193
|
+
_id: "0",
|
|
194
|
+
message: { role: "user", content: "Hello" },
|
|
195
|
+
order: 1,
|
|
196
|
+
} as MessageDoc,
|
|
197
|
+
{
|
|
198
|
+
_id: "1",
|
|
199
|
+
message: {
|
|
200
|
+
role: "assistant",
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: "tool-call",
|
|
204
|
+
toolCallId: "call_orphaned",
|
|
205
|
+
toolName: "test",
|
|
206
|
+
input: {},
|
|
207
|
+
args: {},
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
order: 1,
|
|
212
|
+
} as MessageDoc,
|
|
213
|
+
{
|
|
214
|
+
_id: "2",
|
|
215
|
+
message: {
|
|
216
|
+
role: "tool",
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: "tool-result",
|
|
220
|
+
toolCallId: "result_orphaned",
|
|
221
|
+
result: "orphaned",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
order: 2,
|
|
226
|
+
} as MessageDoc,
|
|
227
|
+
{
|
|
228
|
+
_id: "3",
|
|
229
|
+
message: { role: "assistant", content: "I'll help you with that" },
|
|
230
|
+
order: 1,
|
|
231
|
+
} as MessageDoc,
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const result = filterOutOrphanedToolMessages(messages);
|
|
235
|
+
expect(result).toHaveLength(2);
|
|
236
|
+
expect(result[0]._id).toBe("0");
|
|
237
|
+
expect(result[1]._id).toBe("3");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should keep tool calls with approval responses (but no tool-result yet)", () => {
|
|
241
|
+
const messages: MessageDoc[] = [
|
|
242
|
+
{
|
|
243
|
+
_id: "1",
|
|
244
|
+
message: {
|
|
245
|
+
role: "assistant",
|
|
246
|
+
content: [
|
|
247
|
+
{ type: "text", text: "I'll run the dangerous tool" },
|
|
248
|
+
{
|
|
249
|
+
type: "tool-call",
|
|
250
|
+
toolCallId: "call_123",
|
|
251
|
+
toolName: "dangerousTool",
|
|
252
|
+
input: { action: "delete" },
|
|
253
|
+
args: { action: "delete" },
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
type: "tool-approval-request",
|
|
257
|
+
toolCallId: "call_123",
|
|
258
|
+
approvalId: "approval_456",
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
order: 1,
|
|
263
|
+
} as MessageDoc,
|
|
264
|
+
{
|
|
265
|
+
_id: "2",
|
|
266
|
+
message: {
|
|
267
|
+
role: "tool",
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: "tool-approval-response",
|
|
271
|
+
approvalId: "approval_456",
|
|
272
|
+
approved: true,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
order: 2,
|
|
277
|
+
} as MessageDoc,
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const result = filterOutOrphanedToolMessages(messages);
|
|
281
|
+
expect(result).toHaveLength(2);
|
|
282
|
+
// The assistant message should still contain the tool-call
|
|
283
|
+
expect(result[0]._id).toBe("1");
|
|
284
|
+
const assistantContent = result[0].message?.content;
|
|
285
|
+
expect(Array.isArray(assistantContent)).toBe(true);
|
|
286
|
+
if (Array.isArray(assistantContent)) {
|
|
287
|
+
const toolCall = assistantContent.find((p) => p.type === "tool-call");
|
|
288
|
+
expect(toolCall).toBeDefined();
|
|
289
|
+
expect(toolCall?.toolCallId).toBe("call_123");
|
|
290
|
+
}
|
|
291
|
+
// The tool message with approval response should be kept
|
|
292
|
+
expect(result[1]._id).toBe("2");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should filter out tool calls with approval request but NO approval response", () => {
|
|
296
|
+
const messages: MessageDoc[] = [
|
|
297
|
+
{
|
|
298
|
+
_id: "1",
|
|
299
|
+
message: {
|
|
300
|
+
role: "assistant",
|
|
301
|
+
content: [
|
|
302
|
+
{ type: "text", text: "I'll run the dangerous tool" },
|
|
303
|
+
{
|
|
304
|
+
type: "tool-call",
|
|
305
|
+
toolCallId: "call_123",
|
|
306
|
+
toolName: "dangerousTool",
|
|
307
|
+
input: { action: "delete" },
|
|
308
|
+
args: { action: "delete" },
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
type: "tool-approval-request",
|
|
312
|
+
toolCallId: "call_123",
|
|
313
|
+
approvalId: "approval_456",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
order: 1,
|
|
318
|
+
} as MessageDoc,
|
|
319
|
+
// No approval response provided
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
const result = filterOutOrphanedToolMessages(messages);
|
|
323
|
+
expect(result).toHaveLength(1);
|
|
324
|
+
// The assistant message should have the tool-call filtered out
|
|
325
|
+
const assistantContent = result[0].message?.content;
|
|
326
|
+
expect(Array.isArray(assistantContent)).toBe(true);
|
|
327
|
+
if (Array.isArray(assistantContent)) {
|
|
328
|
+
// Text and approval-request should remain, but tool-call should be filtered
|
|
329
|
+
expect(assistantContent).toHaveLength(2);
|
|
330
|
+
expect(assistantContent.find((p) => p.type === "text")).toBeDefined();
|
|
331
|
+
expect(
|
|
332
|
+
assistantContent.find((p) => p.type === "tool-approval-request"),
|
|
333
|
+
).toBeDefined();
|
|
334
|
+
expect(
|
|
335
|
+
assistantContent.find((p) => p.type === "tool-call"),
|
|
336
|
+
).toBeUndefined();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("should handle mix of tool calls with results and with approvals", () => {
|
|
341
|
+
const messages: MessageDoc[] = [
|
|
342
|
+
{
|
|
343
|
+
_id: "1",
|
|
344
|
+
message: {
|
|
345
|
+
role: "assistant",
|
|
346
|
+
content: [
|
|
347
|
+
{
|
|
348
|
+
type: "tool-call",
|
|
349
|
+
toolCallId: "call_with_result",
|
|
350
|
+
toolName: "safeTool",
|
|
351
|
+
input: {},
|
|
352
|
+
args: {},
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
type: "tool-call",
|
|
356
|
+
toolCallId: "call_with_approval",
|
|
357
|
+
toolName: "dangerousTool",
|
|
358
|
+
input: {},
|
|
359
|
+
args: {},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
type: "tool-approval-request",
|
|
363
|
+
toolCallId: "call_with_approval",
|
|
364
|
+
approvalId: "approval_789",
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
order: 1,
|
|
369
|
+
} as MessageDoc,
|
|
370
|
+
{
|
|
371
|
+
_id: "2",
|
|
372
|
+
message: {
|
|
373
|
+
role: "tool",
|
|
374
|
+
content: [
|
|
375
|
+
{
|
|
376
|
+
type: "tool-result",
|
|
377
|
+
toolCallId: "call_with_result",
|
|
378
|
+
result: "success",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
type: "tool-approval-response",
|
|
382
|
+
approvalId: "approval_789",
|
|
383
|
+
approved: true,
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
order: 2,
|
|
388
|
+
} as MessageDoc,
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
const result = filterOutOrphanedToolMessages(messages);
|
|
392
|
+
expect(result).toHaveLength(2);
|
|
393
|
+
// Both tool calls should be kept
|
|
394
|
+
const assistantContent = result[0].message?.content;
|
|
395
|
+
expect(Array.isArray(assistantContent)).toBe(true);
|
|
396
|
+
if (Array.isArray(assistantContent)) {
|
|
397
|
+
const toolCalls = assistantContent.filter(
|
|
398
|
+
(p) => p.type === "tool-call",
|
|
399
|
+
);
|
|
400
|
+
expect(toolCalls).toHaveLength(2);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("fetchContextMessages", () => {
|
|
406
|
+
it("should throw error if neither userId nor threadId provided", async () => {
|
|
407
|
+
await expect(
|
|
408
|
+
fetchContextMessages(mockCtx, components.agent, {
|
|
409
|
+
userId: undefined,
|
|
410
|
+
threadId: undefined,
|
|
411
|
+
contextOptions: {},
|
|
412
|
+
}),
|
|
413
|
+
).rejects.toThrow("Specify userId or threadId");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should fetch recent messages when threadId provided", async () => {
|
|
417
|
+
const mockPage = [
|
|
418
|
+
createMockMessageDoc("2", "assistant", "Hi!", 2),
|
|
419
|
+
createMockMessageDoc("1", "user", "Hello", 1),
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
(
|
|
423
|
+
mockCtx.runQuery as MockedFunction<ActionCtx["runQuery"]>
|
|
424
|
+
).mockResolvedValue({
|
|
425
|
+
page: mockPage,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const result = await fetchContextMessages(mockCtx, components.agent, {
|
|
429
|
+
userId: undefined,
|
|
430
|
+
threadId: "thread123",
|
|
431
|
+
contextOptions: { recentMessages: 10 },
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(mockCtx.runQuery).toHaveBeenCalledWith(expect.anything(), {
|
|
435
|
+
threadId: "thread123",
|
|
436
|
+
paginationOpts: { numItems: 10, cursor: null },
|
|
437
|
+
order: "desc",
|
|
438
|
+
excludeToolMessages: undefined,
|
|
439
|
+
statuses: ["success"],
|
|
440
|
+
upToAndIncludingMessageId: undefined,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(result.length).toBe(2);
|
|
444
|
+
expect(result[0]._id).toBe("1"); // Should be reversed back to asc order
|
|
445
|
+
expect(result[1]._id).toBe("2");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("should skip recent messages when recentMessages is 0", async () => {
|
|
449
|
+
const result = await fetchContextMessages(mockCtx, components.agent, {
|
|
450
|
+
userId: "user123",
|
|
451
|
+
threadId: "thread123",
|
|
452
|
+
contextOptions: { recentMessages: 0 },
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
expect(mockCtx.runQuery).not.toHaveBeenCalled();
|
|
456
|
+
expect(result).toEqual([]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("should perform search when searchOptions provided", async () => {
|
|
460
|
+
const searchResults = [
|
|
461
|
+
createMockMessageDoc("search1", "user", "Search result", 0),
|
|
462
|
+
];
|
|
463
|
+
|
|
464
|
+
(
|
|
465
|
+
mockCtx.runAction as MockedFunction<ActionCtx["runAction"]>
|
|
466
|
+
).mockResolvedValue(searchResults);
|
|
467
|
+
|
|
468
|
+
const result = await fetchContextMessages(mockCtx, components.agent, {
|
|
469
|
+
userId: "user123",
|
|
470
|
+
threadId: "thread123",
|
|
471
|
+
searchText: "test query",
|
|
472
|
+
contextOptions: {
|
|
473
|
+
recentMessages: 0,
|
|
474
|
+
searchOptions: {
|
|
475
|
+
textSearch: true,
|
|
476
|
+
limit: 5,
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(result.length).toBe(1);
|
|
482
|
+
expect(result[0]._id).toBe("search1");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("should throw error when trying to search in non-action context", async () => {
|
|
486
|
+
const mockQueryCtx = {
|
|
487
|
+
runQuery: vi.fn().mockResolvedValue({ page: [] }),
|
|
488
|
+
// No runAction method
|
|
489
|
+
storage: {} as StorageReader,
|
|
490
|
+
} as QueryCtx;
|
|
491
|
+
|
|
492
|
+
await expect(
|
|
493
|
+
fetchContextMessages(mockQueryCtx, components.agent, {
|
|
494
|
+
userId: "user123",
|
|
495
|
+
threadId: "thread123",
|
|
496
|
+
contextOptions: {
|
|
497
|
+
searchOptions: {
|
|
498
|
+
textSearch: true,
|
|
499
|
+
limit: 5,
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
}),
|
|
503
|
+
).rejects.toThrow("searchUserMessages only works in an action");
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe("fetchContextWithPrompt", () => {
|
|
508
|
+
const baseArgs = {
|
|
509
|
+
userId: "user123",
|
|
510
|
+
threadId: "thread123",
|
|
511
|
+
agentName: "test-agent",
|
|
512
|
+
contextOptions: {},
|
|
513
|
+
usageHandler: undefined,
|
|
514
|
+
callSettings: {},
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
beforeEach(() => {
|
|
518
|
+
// Mock fetchContextMessages to return empty array by default
|
|
519
|
+
vi.mocked(mockCtx.runQuery).mockResolvedValue({ page: [] });
|
|
520
|
+
vi.mocked(mockCtx.runAction).mockResolvedValue([]);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("should handle string prompt correctly", async () => {
|
|
524
|
+
const result = await fetchContextWithPrompt(mockCtx, components.agent, {
|
|
525
|
+
...baseArgs,
|
|
526
|
+
prompt: "Hello, how are you?",
|
|
527
|
+
messages: undefined,
|
|
528
|
+
promptMessageId: undefined,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(result.messages).toHaveLength(1);
|
|
532
|
+
expect(result.messages[0]).toEqual({
|
|
533
|
+
role: "user",
|
|
534
|
+
content: "Hello, how are you?",
|
|
535
|
+
});
|
|
536
|
+
expect(result.order).toBeUndefined();
|
|
537
|
+
expect(result.stepOrder).toBeUndefined();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("should handle array prompt correctly", async () => {
|
|
541
|
+
const promptMessages: ModelMessage[] = [
|
|
542
|
+
{ role: "user", content: "Hello" },
|
|
543
|
+
{ role: "assistant", content: "Hi there!" },
|
|
544
|
+
{ role: "user", content: "How are you?" },
|
|
545
|
+
];
|
|
546
|
+
|
|
547
|
+
const result = await fetchContextWithPrompt(mockCtx, components.agent, {
|
|
548
|
+
...baseArgs,
|
|
549
|
+
prompt: promptMessages,
|
|
550
|
+
messages: undefined,
|
|
551
|
+
promptMessageId: undefined,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
expect(result.messages).toHaveLength(3);
|
|
555
|
+
expect(result.messages).toEqual(promptMessages);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("should combine context messages with prompt", async () => {
|
|
559
|
+
const contextMessages: MessageDoc[] = [
|
|
560
|
+
{
|
|
561
|
+
_id: "ctx1",
|
|
562
|
+
message: { role: "user", content: "Context message 1" },
|
|
563
|
+
order: 1,
|
|
564
|
+
} as MessageDoc,
|
|
565
|
+
{
|
|
566
|
+
_id: "ctx2",
|
|
567
|
+
message: { role: "assistant", content: "Context response 1" },
|
|
568
|
+
order: 2,
|
|
569
|
+
} as MessageDoc,
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
// Mock the internal fetchContextMessages call
|
|
573
|
+
vi.mocked(mockCtx.runQuery).mockResolvedValue({
|
|
574
|
+
page: [...contextMessages].reverse(),
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const result = await fetchContextWithPrompt(mockCtx, components.agent, {
|
|
578
|
+
...baseArgs,
|
|
579
|
+
prompt: "New prompt",
|
|
580
|
+
messages: undefined,
|
|
581
|
+
promptMessageId: undefined,
|
|
582
|
+
contextOptions: { recentMessages: 10 },
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
expect(result.messages).toHaveLength(3);
|
|
586
|
+
expect(result.messages[0].content).toBe("Context message 1");
|
|
587
|
+
expect(result.messages[1].content).toBe("Context response 1");
|
|
588
|
+
expect(result.messages[2]).toEqual({
|
|
589
|
+
role: "user",
|
|
590
|
+
content: "New prompt",
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("should handle input messages correctly", async () => {
|
|
595
|
+
const inputMessages: ModelMessage[] = [
|
|
596
|
+
{ role: "user", content: "Input message 1" },
|
|
597
|
+
{ role: "assistant", content: "Input response 1" },
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
const result = await fetchContextWithPrompt(mockCtx, components.agent, {
|
|
601
|
+
...baseArgs,
|
|
602
|
+
prompt: "Final prompt",
|
|
603
|
+
messages: inputMessages,
|
|
604
|
+
promptMessageId: undefined,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
expect(result.messages).toHaveLength(3);
|
|
608
|
+
expect(result.messages[0]).toEqual(inputMessages[0]);
|
|
609
|
+
expect(result.messages[1]).toEqual(inputMessages[1]);
|
|
610
|
+
expect(result.messages[2]).toEqual({
|
|
611
|
+
role: "user",
|
|
612
|
+
content: "Final prompt",
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it("should splice prompt messages when promptMessageId provided", async () => {
|
|
617
|
+
const contextMessages: MessageDoc[] = [
|
|
618
|
+
{
|
|
619
|
+
_id: "msg1",
|
|
620
|
+
message: { role: "user", content: "Before prompt" },
|
|
621
|
+
order: 1,
|
|
622
|
+
} as MessageDoc,
|
|
623
|
+
{
|
|
624
|
+
_id: "prompt-msg",
|
|
625
|
+
message: { role: "user", content: "Original prompt" },
|
|
626
|
+
order: 2,
|
|
627
|
+
} as MessageDoc,
|
|
628
|
+
{
|
|
629
|
+
_id: "msg3",
|
|
630
|
+
message: { role: "assistant", content: "After prompt" },
|
|
631
|
+
order: 3,
|
|
632
|
+
} as MessageDoc,
|
|
633
|
+
];
|
|
634
|
+
|
|
635
|
+
vi.mocked(mockCtx.runQuery).mockResolvedValue({
|
|
636
|
+
page: [...contextMessages].reverse(),
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const result = await fetchContextWithPrompt(mockCtx, components.agent, {
|
|
640
|
+
...baseArgs,
|
|
641
|
+
prompt: "New replacement prompt",
|
|
642
|
+
messages: undefined,
|
|
643
|
+
promptMessageId: "prompt-msg",
|
|
644
|
+
contextOptions: { recentMessages: 10 },
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
expect(result.messages).toHaveLength(3);
|
|
648
|
+
expect(result.messages[0].content).toBe("Before prompt");
|
|
649
|
+
expect(result.messages[1]).toEqual({
|
|
650
|
+
role: "user",
|
|
651
|
+
content: "New replacement prompt",
|
|
652
|
+
});
|
|
653
|
+
expect(result.messages[2].content).toBe("After prompt");
|
|
654
|
+
expect(result.order).toBe(2);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("should use original prompt message when no new prompt provided", async () => {
|
|
658
|
+
const contextMessages: MessageDoc[] = [
|
|
659
|
+
{
|
|
660
|
+
_id: "msg1",
|
|
661
|
+
message: { role: "user", content: "Before prompt" },
|
|
662
|
+
order: 1,
|
|
663
|
+
} as MessageDoc,
|
|
664
|
+
{
|
|
665
|
+
_id: "prompt-msg",
|
|
666
|
+
message: { role: "user", content: "Original prompt" },
|
|
667
|
+
order: 2,
|
|
668
|
+
} as MessageDoc,
|
|
669
|
+
{
|
|
670
|
+
_id: "msg3",
|
|
671
|
+
message: { role: "assistant", content: "After prompt" },
|
|
672
|
+
order: 3,
|
|
673
|
+
} as MessageDoc,
|
|
674
|
+
];
|
|
675
|
+
|
|
676
|
+
vi.mocked(mockCtx.runQuery).mockResolvedValue({
|
|
677
|
+
page: [...contextMessages].reverse(),
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const result = await fetchContextWithPrompt(mockCtx, components.agent, {
|
|
681
|
+
...baseArgs,
|
|
682
|
+
prompt: undefined,
|
|
683
|
+
messages: undefined,
|
|
684
|
+
promptMessageId: "prompt-msg",
|
|
685
|
+
contextOptions: { recentMessages: 10 },
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
expect(result.messages).toHaveLength(3);
|
|
689
|
+
expect(result.messages[0].content).toBe("Before prompt");
|
|
690
|
+
expect(result.messages[1].content).toBe("Original prompt");
|
|
691
|
+
expect(result.messages[2].content).toBe("After prompt");
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("should handle complex message ordering correctly", async () => {
|
|
695
|
+
const contextMessages: MessageDoc[] = [
|
|
696
|
+
{
|
|
697
|
+
_id: "ctx1",
|
|
698
|
+
message: { role: "user", content: "Context 1" },
|
|
699
|
+
order: 1,
|
|
700
|
+
} as MessageDoc,
|
|
701
|
+
{
|
|
702
|
+
_id: "prompt-msg",
|
|
703
|
+
message: { role: "user", content: "Prompt" },
|
|
704
|
+
order: 3,
|
|
705
|
+
} as MessageDoc,
|
|
706
|
+
{
|
|
707
|
+
_id: "ctx2",
|
|
708
|
+
message: { role: "assistant", content: "Context 2" },
|
|
709
|
+
order: 5,
|
|
710
|
+
} as MessageDoc,
|
|
711
|
+
];
|
|
712
|
+
|
|
713
|
+
vi.mocked(mockCtx.runQuery).mockResolvedValue({
|
|
714
|
+
page: [...contextMessages].reverse(),
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
const inputMessages: ModelMessage[] = [
|
|
718
|
+
{ role: "user", content: "Input message" },
|
|
719
|
+
];
|
|
720
|
+
|
|
721
|
+
const result = await fetchContextWithPrompt(mockCtx, components.agent, {
|
|
722
|
+
...baseArgs,
|
|
723
|
+
prompt: "New prompt",
|
|
724
|
+
messages: inputMessages,
|
|
725
|
+
promptMessageId: "prompt-msg",
|
|
726
|
+
contextOptions: { recentMessages: 10 },
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
expect(result.messages).toHaveLength(4);
|
|
730
|
+
expect(result.messages[0].content).toBe("Context 1"); // Pre-prompt
|
|
731
|
+
expect(result.messages[1].content).toBe("Input message"); // Input messages
|
|
732
|
+
expect(result.messages[2].content).toBe("New prompt"); // New prompt
|
|
733
|
+
expect(result.messages[3].content).toBe("Context 2"); // Post-prompt
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("should handle empty context and messages", async () => {
|
|
737
|
+
const result = await fetchContextWithPrompt(mockCtx, components.agent, {
|
|
738
|
+
...baseArgs,
|
|
739
|
+
prompt: undefined,
|
|
740
|
+
messages: undefined,
|
|
741
|
+
promptMessageId: undefined,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
expect(result.messages).toHaveLength(0);
|
|
745
|
+
expect(result.order).toBeUndefined();
|
|
746
|
+
expect(result.stepOrder).toBeUndefined();
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
describe("fetchContextWithPrompt - Integration Tests", () => {
|
|
751
|
+
const baseArgs = {
|
|
752
|
+
userId: "user123",
|
|
753
|
+
threadId: "thread123",
|
|
754
|
+
agentName: "test-agent",
|
|
755
|
+
contextOptions: {},
|
|
756
|
+
usageHandler: undefined,
|
|
757
|
+
callSettings: {},
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
it("should fetch and combine real messages with prompt", async () => {
|
|
761
|
+
const threadId = await createTestThread("user123");
|
|
762
|
+
|
|
763
|
+
await createTestMessages(threadId, "user123", [
|
|
764
|
+
{ role: "user", content: "Hello", order: 1 },
|
|
765
|
+
{ role: "assistant", content: "Hi there!", order: 2 },
|
|
766
|
+
{ role: "user", content: "How are you?", order: 3 },
|
|
767
|
+
]);
|
|
768
|
+
|
|
769
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
770
|
+
...baseArgs,
|
|
771
|
+
threadId,
|
|
772
|
+
prompt: "What's the weather?",
|
|
773
|
+
messages: undefined,
|
|
774
|
+
promptMessageId: undefined,
|
|
775
|
+
contextOptions: { recentMessages: 10 },
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
expect(result.messages).toHaveLength(4);
|
|
779
|
+
expect(result.messages[0].content).toBe("Hello");
|
|
780
|
+
expect(result.messages[1].content).toBe("Hi there!");
|
|
781
|
+
expect(result.messages[2].content).toBe("How are you?");
|
|
782
|
+
expect(result.messages[3]).toEqual({
|
|
783
|
+
role: "user",
|
|
784
|
+
content: "What's the weather?",
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("should handle prompt message replacement in real data", async () => {
|
|
789
|
+
const threadId = await createTestThread("user456");
|
|
790
|
+
|
|
791
|
+
// Create messages and capture the prompt message ID
|
|
792
|
+
const messages = [
|
|
793
|
+
{ role: "user" as const, content: "Before prompt" },
|
|
794
|
+
{ role: "user" as const, content: "Original prompt" },
|
|
795
|
+
{ role: "assistant" as const, content: "Assistant response" },
|
|
796
|
+
];
|
|
797
|
+
|
|
798
|
+
const { messages: savedMessages } = await t.run(async (mutCtx) => {
|
|
799
|
+
return await saveMessages(mutCtx, components.agent, {
|
|
800
|
+
threadId,
|
|
801
|
+
userId: "user456",
|
|
802
|
+
messages: messages.map((msg) => ({
|
|
803
|
+
role: msg.role,
|
|
804
|
+
content: msg.content,
|
|
805
|
+
})),
|
|
806
|
+
metadata: messages.map(() => ({})),
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
const promptMessageId = savedMessages[1]._id; // The prompt message
|
|
811
|
+
|
|
812
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
813
|
+
...baseArgs,
|
|
814
|
+
userId: "user456",
|
|
815
|
+
threadId,
|
|
816
|
+
prompt: "New replacement prompt",
|
|
817
|
+
messages: undefined,
|
|
818
|
+
promptMessageId,
|
|
819
|
+
contextOptions: { recentMessages: 10 },
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
expect(result.messages).toHaveLength(3);
|
|
823
|
+
expect(result.messages[0].content).toBe("Before prompt");
|
|
824
|
+
expect(result.messages[1]).toEqual({
|
|
825
|
+
role: "user",
|
|
826
|
+
content: "New replacement prompt",
|
|
827
|
+
});
|
|
828
|
+
expect(result.messages[2].content).toBe("Assistant response");
|
|
829
|
+
// The prompt is the second user message, each on a new order.
|
|
830
|
+
expect(result.order).toBe(1);
|
|
831
|
+
expect(result.stepOrder).toBe(0);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it("should combine input messages with context and prompt", async () => {
|
|
835
|
+
const threadId = await createTestThread("user789");
|
|
836
|
+
|
|
837
|
+
await createTestMessages(threadId, "user789", [
|
|
838
|
+
{ role: "user", content: "Context message", order: 1 },
|
|
839
|
+
{ role: "assistant", content: "Context response", order: 2 },
|
|
840
|
+
]);
|
|
841
|
+
|
|
842
|
+
const inputMessages: ModelMessage[] = [
|
|
843
|
+
{ role: "user", content: "Input message 1" },
|
|
844
|
+
{ role: "user", content: "Input message 2" },
|
|
845
|
+
];
|
|
846
|
+
|
|
847
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
848
|
+
...baseArgs,
|
|
849
|
+
userId: "user789",
|
|
850
|
+
threadId,
|
|
851
|
+
prompt: "Final prompt",
|
|
852
|
+
messages: inputMessages,
|
|
853
|
+
promptMessageId: undefined,
|
|
854
|
+
contextOptions: { recentMessages: 10 },
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
expect(result.messages).toHaveLength(5);
|
|
858
|
+
expect(result.messages[0].content).toBe("Context message");
|
|
859
|
+
expect(result.messages[1].content).toBe("Context response");
|
|
860
|
+
expect(result.messages[2].content).toBe("Input message 1");
|
|
861
|
+
expect(result.messages[3].content).toBe("Input message 2");
|
|
862
|
+
expect(result.messages[4]).toEqual({
|
|
863
|
+
role: "user",
|
|
864
|
+
content: "Final prompt",
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it("should respect recentMessages limit", async () => {
|
|
869
|
+
const threadId = await createTestThread("user999");
|
|
870
|
+
|
|
871
|
+
// Create 5 messages but only fetch the most recent 2
|
|
872
|
+
await createTestMessages(threadId, "user999", [
|
|
873
|
+
{ role: "user", content: "Message 1", order: 1 },
|
|
874
|
+
{ role: "assistant", content: "Response 1", order: 2 },
|
|
875
|
+
{ role: "user", content: "Message 2", order: 3 },
|
|
876
|
+
{ role: "assistant", content: "Response 2", order: 4 },
|
|
877
|
+
{ role: "user", content: "Message 3", order: 5 },
|
|
878
|
+
]);
|
|
879
|
+
|
|
880
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
881
|
+
...baseArgs,
|
|
882
|
+
userId: "user999",
|
|
883
|
+
threadId,
|
|
884
|
+
prompt: "New prompt",
|
|
885
|
+
messages: undefined,
|
|
886
|
+
promptMessageId: undefined,
|
|
887
|
+
contextOptions: { recentMessages: 2 }, // Only fetch 2 most recent
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
expect(result.messages).toHaveLength(3); // 2 context + 1 prompt
|
|
891
|
+
expect(result.messages[0].content).toBe("Response 2"); // 4th message
|
|
892
|
+
expect(result.messages[1].content).toBe("Message 3"); // 5th message
|
|
893
|
+
expect(result.messages[2]).toEqual({
|
|
894
|
+
role: "user",
|
|
895
|
+
content: "New prompt",
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it("should handle empty thread gracefully", async () => {
|
|
900
|
+
const threadId = await createTestThread("user000");
|
|
901
|
+
|
|
902
|
+
// Don't create any messages
|
|
903
|
+
|
|
904
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
905
|
+
...baseArgs,
|
|
906
|
+
userId: "user000",
|
|
907
|
+
threadId,
|
|
908
|
+
prompt: "Only prompt",
|
|
909
|
+
messages: undefined,
|
|
910
|
+
promptMessageId: undefined,
|
|
911
|
+
contextOptions: { recentMessages: 10 },
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
expect(result.messages).toHaveLength(1);
|
|
915
|
+
expect(result.messages[0]).toEqual({
|
|
916
|
+
role: "user",
|
|
917
|
+
content: "Only prompt",
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
describe("fetchContextWithPrompt - contextHandler Tests", () => {
|
|
923
|
+
const baseArgs = {
|
|
924
|
+
userId: "user123",
|
|
925
|
+
threadId: "thread123",
|
|
926
|
+
agentName: "test-agent",
|
|
927
|
+
contextOptions: {},
|
|
928
|
+
usageHandler: undefined,
|
|
929
|
+
callSettings: {},
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
it("should use custom contextHandler to reorder messages", async () => {
|
|
933
|
+
const threadId = await createTestThread("userContext");
|
|
934
|
+
|
|
935
|
+
await createTestMessages(threadId, "userContext", [
|
|
936
|
+
{ role: "user", content: "Recent message 1", order: 1 },
|
|
937
|
+
{ role: "assistant", content: "Recent response 1", order: 2 },
|
|
938
|
+
]);
|
|
939
|
+
|
|
940
|
+
// Create a contextHandler that puts inputMessages first, then inputPrompt, then recent
|
|
941
|
+
const contextHandler = vi.fn(async (ctx, args) => {
|
|
942
|
+
return [
|
|
943
|
+
...args.inputMessages,
|
|
944
|
+
...args.inputPrompt,
|
|
945
|
+
...args.recent,
|
|
946
|
+
...args.search,
|
|
947
|
+
...args.existingResponses,
|
|
948
|
+
];
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
952
|
+
...baseArgs,
|
|
953
|
+
userId: "userContext",
|
|
954
|
+
threadId,
|
|
955
|
+
prompt: "Custom prompt",
|
|
956
|
+
messages: [{ role: "user", content: "Input message" }],
|
|
957
|
+
promptMessageId: undefined,
|
|
958
|
+
contextOptions: { recentMessages: 10 },
|
|
959
|
+
contextHandler,
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
// Verify contextHandler was called with correct arguments
|
|
963
|
+
expect(contextHandler).toHaveBeenCalledWith(
|
|
964
|
+
ctx,
|
|
965
|
+
expect.objectContaining({
|
|
966
|
+
search: [], // No search performed in this test
|
|
967
|
+
recent: expect.arrayContaining([
|
|
968
|
+
expect.objectContaining({ content: "Recent message 1" }),
|
|
969
|
+
expect.objectContaining({ content: "Recent response 1" }),
|
|
970
|
+
]),
|
|
971
|
+
inputMessages: expect.arrayContaining([
|
|
972
|
+
expect.objectContaining({ content: "Input message" }),
|
|
973
|
+
]),
|
|
974
|
+
inputPrompt: expect.arrayContaining([
|
|
975
|
+
expect.objectContaining({ content: "Custom prompt" }),
|
|
976
|
+
]),
|
|
977
|
+
existingResponses: [], // No existing responses in this test
|
|
978
|
+
userId: "userContext",
|
|
979
|
+
threadId,
|
|
980
|
+
}),
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
// Result should follow the custom order: inputMessages, inputPrompt, recent
|
|
984
|
+
expect(result.messages).toHaveLength(4);
|
|
985
|
+
expect(result.messages[0].content).toBe("Input message"); // inputMessages
|
|
986
|
+
expect(result.messages[1].content).toBe("Custom prompt"); // inputPrompt
|
|
987
|
+
expect(result.messages[2].content).toBe("Recent message 1"); // recent
|
|
988
|
+
expect(result.messages[3].content).toBe("Recent response 1"); // recent
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it("should allow contextHandler to filter out messages", async () => {
|
|
992
|
+
const threadId = await createTestThread("userFilter");
|
|
993
|
+
|
|
994
|
+
await createTestMessages(threadId, "userFilter", [
|
|
995
|
+
{ role: "user", content: "Keep this message", order: 1 },
|
|
996
|
+
{ role: "assistant", content: "Filter this out", order: 2 },
|
|
997
|
+
{ role: "user", content: "Keep this too", order: 3 },
|
|
998
|
+
]);
|
|
999
|
+
|
|
1000
|
+
// Create a contextHandler that filters out assistant messages
|
|
1001
|
+
const contextHandler = vi.fn(async (ctx, args) => {
|
|
1002
|
+
const allMessages = [
|
|
1003
|
+
...args.search,
|
|
1004
|
+
...args.recent,
|
|
1005
|
+
...args.inputMessages,
|
|
1006
|
+
...args.inputPrompt,
|
|
1007
|
+
...args.existingResponses,
|
|
1008
|
+
];
|
|
1009
|
+
|
|
1010
|
+
// Filter out assistant messages
|
|
1011
|
+
return allMessages.filter((msg) => msg.role !== "assistant");
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
1015
|
+
...baseArgs,
|
|
1016
|
+
userId: "userFilter",
|
|
1017
|
+
threadId,
|
|
1018
|
+
prompt: "Filter prompt",
|
|
1019
|
+
messages: undefined,
|
|
1020
|
+
promptMessageId: undefined,
|
|
1021
|
+
contextOptions: { recentMessages: 10 },
|
|
1022
|
+
contextHandler,
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// Should only have user messages and the prompt
|
|
1026
|
+
expect(result.messages).toHaveLength(3);
|
|
1027
|
+
expect(result.messages[0].content).toBe("Keep this message");
|
|
1028
|
+
expect(result.messages[1].content).toBe("Keep this too");
|
|
1029
|
+
expect(result.messages[2].content).toBe("Filter prompt");
|
|
1030
|
+
|
|
1031
|
+
// Should not contain the filtered assistant message
|
|
1032
|
+
expect(
|
|
1033
|
+
result.messages.find((m) => m.content === "Filter this out"),
|
|
1034
|
+
).toBeUndefined();
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
it("should allow contextHandler to add custom messages", async () => {
|
|
1038
|
+
const threadId = await createTestThread("userCustom");
|
|
1039
|
+
|
|
1040
|
+
await createTestMessages(threadId, "userCustom", [
|
|
1041
|
+
{ role: "user", content: "Original message", order: 1 },
|
|
1042
|
+
]);
|
|
1043
|
+
|
|
1044
|
+
// Create a contextHandler that adds a custom system message
|
|
1045
|
+
const contextHandler = vi.fn(async (ctx, args) => {
|
|
1046
|
+
const customSystemMessage = {
|
|
1047
|
+
role: "system" as const,
|
|
1048
|
+
content: "This is a custom system message added by contextHandler",
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
return [customSystemMessage, ...args.recent, ...args.inputPrompt];
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
1055
|
+
...baseArgs,
|
|
1056
|
+
userId: "userCustom",
|
|
1057
|
+
threadId,
|
|
1058
|
+
prompt: "Test prompt",
|
|
1059
|
+
messages: undefined,
|
|
1060
|
+
promptMessageId: undefined,
|
|
1061
|
+
contextOptions: { recentMessages: 10 },
|
|
1062
|
+
contextHandler,
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
expect(result.messages).toHaveLength(3);
|
|
1066
|
+
expect(result.messages[0].role).toBe("system");
|
|
1067
|
+
expect(result.messages[0].content).toBe(
|
|
1068
|
+
"This is a custom system message added by contextHandler",
|
|
1069
|
+
);
|
|
1070
|
+
expect(result.messages[1].content).toBe("Original message");
|
|
1071
|
+
expect(result.messages[2].content).toBe("Test prompt");
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
it("should work with search messages in contextHandler", async () => {
|
|
1075
|
+
const threadId = await createTestThread("userSearch");
|
|
1076
|
+
|
|
1077
|
+
// Create multiple messages for search to find
|
|
1078
|
+
await createTestMessages(threadId, "userSearch", [
|
|
1079
|
+
{ role: "user", content: "Searchable content about cats", order: 1 },
|
|
1080
|
+
{ role: "assistant", content: "Response about cats", order: 2 },
|
|
1081
|
+
{ role: "user", content: "Recent non-searchable message", order: 3 },
|
|
1082
|
+
]);
|
|
1083
|
+
|
|
1084
|
+
const contextHandler = vi.fn(async (ctx, args) => {
|
|
1085
|
+
// Put search messages first, then recent, then prompt
|
|
1086
|
+
return [...args.search, ...args.recent, ...args.inputPrompt];
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
1090
|
+
...baseArgs,
|
|
1091
|
+
userId: "userSearch",
|
|
1092
|
+
threadId,
|
|
1093
|
+
prompt: "Tell me about cats",
|
|
1094
|
+
messages: undefined,
|
|
1095
|
+
promptMessageId: undefined,
|
|
1096
|
+
contextOptions: {
|
|
1097
|
+
recentMessages: 1, // Only get 1 recent message
|
|
1098
|
+
searchOptions: {
|
|
1099
|
+
textSearch: true,
|
|
1100
|
+
limit: 2,
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1103
|
+
contextHandler,
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
expect(contextHandler).toHaveBeenCalledWith(
|
|
1107
|
+
ctx,
|
|
1108
|
+
expect.objectContaining({
|
|
1109
|
+
search: expect.any(Array),
|
|
1110
|
+
recent: expect.any(Array),
|
|
1111
|
+
inputPrompt: expect.arrayContaining([
|
|
1112
|
+
expect.objectContaining({ content: "Tell me about cats" }),
|
|
1113
|
+
]),
|
|
1114
|
+
}),
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
// Should have recent + prompt (search may not return results in test environment)
|
|
1118
|
+
expect(result.messages.length).toBeGreaterThanOrEqual(2);
|
|
1119
|
+
expect(result.messages[result.messages.length - 1].content).toBe(
|
|
1120
|
+
"Tell me about cats",
|
|
1121
|
+
);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it("should handle existingResponses in contextHandler when promptMessageId provided", async () => {
|
|
1125
|
+
const threadId = await createTestThread("userResponses");
|
|
1126
|
+
|
|
1127
|
+
const { messages: savedMessages } = await t.run(async (mutCtx) => {
|
|
1128
|
+
return await saveMessages(mutCtx, components.agent, {
|
|
1129
|
+
threadId,
|
|
1130
|
+
userId: "userResponses",
|
|
1131
|
+
messages: [
|
|
1132
|
+
{ role: "user", content: "Before prompt" },
|
|
1133
|
+
{ role: "user", content: "Original prompt" },
|
|
1134
|
+
{ role: "assistant", content: "Existing response 1" },
|
|
1135
|
+
{ role: "assistant", content: "Existing response 2" },
|
|
1136
|
+
],
|
|
1137
|
+
metadata: [{}, {}, {}, {}],
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
const promptMessageId = savedMessages[1]._id; // The prompt message
|
|
1142
|
+
|
|
1143
|
+
const contextHandler = vi.fn(async (ctx, args) => {
|
|
1144
|
+
// Put existing responses first to test they're properly identified
|
|
1145
|
+
return [...args.recent, ...args.existingResponses, ...args.inputPrompt];
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
1149
|
+
...baseArgs,
|
|
1150
|
+
userId: "userResponses",
|
|
1151
|
+
threadId,
|
|
1152
|
+
prompt: "New replacement prompt",
|
|
1153
|
+
messages: undefined,
|
|
1154
|
+
promptMessageId,
|
|
1155
|
+
contextOptions: { recentMessages: 10 },
|
|
1156
|
+
contextHandler,
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
expect(contextHandler).toHaveBeenCalledWith(
|
|
1160
|
+
ctx,
|
|
1161
|
+
expect.objectContaining({
|
|
1162
|
+
recent: expect.arrayContaining([
|
|
1163
|
+
expect.objectContaining({ content: "Before prompt" }),
|
|
1164
|
+
]),
|
|
1165
|
+
existingResponses: expect.arrayContaining([
|
|
1166
|
+
expect.objectContaining({ content: "Existing response 1" }),
|
|
1167
|
+
expect.objectContaining({ content: "Existing response 2" }),
|
|
1168
|
+
]),
|
|
1169
|
+
inputPrompt: expect.arrayContaining([
|
|
1170
|
+
expect.objectContaining({ content: "New replacement prompt" }),
|
|
1171
|
+
]),
|
|
1172
|
+
}),
|
|
1173
|
+
);
|
|
1174
|
+
|
|
1175
|
+
expect(result.messages).toHaveLength(4);
|
|
1176
|
+
expect(result.messages[0].content).toBe("Before prompt");
|
|
1177
|
+
expect(result.messages[1].content).toBe("Existing response 1");
|
|
1178
|
+
expect(result.messages[2].content).toBe("Existing response 2");
|
|
1179
|
+
expect(result.messages[3].content).toBe("New replacement prompt");
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it("should work without contextHandler (default behavior)", async () => {
|
|
1183
|
+
const threadId = await createTestThread("userDefault");
|
|
1184
|
+
|
|
1185
|
+
await createTestMessages(threadId, "userDefault", [
|
|
1186
|
+
{ role: "user", content: "Default order test", order: 1 },
|
|
1187
|
+
]);
|
|
1188
|
+
|
|
1189
|
+
const result = await fetchContextWithPrompt(ctx, components.agent, {
|
|
1190
|
+
...baseArgs,
|
|
1191
|
+
userId: "userDefault",
|
|
1192
|
+
threadId,
|
|
1193
|
+
prompt: "Default prompt",
|
|
1194
|
+
messages: [{ role: "user", content: "Input message" }],
|
|
1195
|
+
promptMessageId: undefined,
|
|
1196
|
+
contextOptions: { recentMessages: 10 },
|
|
1197
|
+
// No contextHandler provided
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// Should follow default order: recent, input, prompt
|
|
1201
|
+
expect(result.messages).toHaveLength(3);
|
|
1202
|
+
expect(result.messages[0].content).toBe("Default order test"); // recent
|
|
1203
|
+
expect(result.messages[1].content).toBe("Input message"); // inputMessages
|
|
1204
|
+
expect(result.messages[2].content).toBe("Default prompt"); // inputPrompt
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
});
|