@convex-dev/agent 0.1.7-alpha.2 → 0.1.7

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.
@@ -19,6 +19,7 @@ import { MockLanguageModelV1 } from "ai/test";
19
19
  import type { LanguageModelV1, LanguageModelV1StreamPart } from "ai";
20
20
  import { simulateReadableStream } from "ai";
21
21
  import { components, initConvexTest } from "./setup.test";
22
+ import { z } from "zod";
22
23
 
23
24
  const schema = defineSchema({});
24
25
  type DataModel = DataModelFromSchemaDefinition<typeof schema>;
@@ -58,6 +59,16 @@ export const createThread = mutation({
58
59
  },
59
60
  });
60
61
 
62
+ export const createThreadMutation = agent.createThreadMutation();
63
+ export const generateObjectAction = agent.asObjectAction({
64
+ schema: z.object({
65
+ prompt: z.any().describe("The prompt passed in"),
66
+ }),
67
+ });
68
+ export const generateTextAction = agent.asTextAction({});
69
+ export const streamTextAction = agent.asTextAction({ stream: true });
70
+ export const saveMessageMutation = agent.asSaveMessagesMutation();
71
+
61
72
  export const createAndGenerate = action({
62
73
  args: {},
63
74
  handler: async (ctx) => {
@@ -71,11 +82,87 @@ export const createAndGenerate = action({
71
82
  },
72
83
  });
73
84
 
85
+ export const continueThreadAction = action({
86
+ args: { threadId: v.string(), userId: v.optional(v.string()) },
87
+ handler: async (ctx, args) => {
88
+ const { thread } = await agent.continueThread(ctx, args);
89
+ return { threadId: thread.threadId };
90
+ },
91
+ });
92
+
93
+ export const generateTextWithThread = action({
94
+ args: {
95
+ threadId: v.string(),
96
+ userId: v.optional(v.string()),
97
+ messages: v.array(v.any()),
98
+ contextOptions: v.optional(v.any()),
99
+ storageOptions: v.optional(v.any()),
100
+ },
101
+ handler: async (ctx, args) => {
102
+ const { thread } = await agent.continueThread(ctx, {
103
+ threadId: args.threadId,
104
+ userId: args.userId,
105
+ });
106
+ const result = await thread.generateText(
107
+ { messages: args.messages },
108
+ {
109
+ contextOptions: args.contextOptions,
110
+ storageOptions: args.storageOptions,
111
+ }
112
+ );
113
+ return { text: result.text };
114
+ },
115
+ });
116
+
117
+ export const generateObjectWithThread = action({
118
+ args: {
119
+ threadId: v.string(),
120
+ userId: v.optional(v.string()),
121
+ prompt: v.string(),
122
+ },
123
+ handler: async (ctx, args) => {
124
+ const { thread } = await agent.continueThread(ctx, {
125
+ threadId: args.threadId,
126
+ userId: args.userId,
127
+ });
128
+ const result = await thread.generateObject({
129
+ prompt: args.prompt,
130
+ schema: z.object({ prompt: z.any() }),
131
+ });
132
+ return { object: result.object };
133
+ },
134
+ });
135
+
136
+ export const fetchContextAction = action({
137
+ args: {
138
+ userId: v.optional(v.string()),
139
+ threadId: v.optional(v.string()),
140
+ messages: v.array(v.any()),
141
+ contextOptions: v.optional(v.any()),
142
+ },
143
+ handler: async (ctx, args) => {
144
+ const context = await agent.fetchContextMessages(ctx, {
145
+ userId: args.userId,
146
+ threadId: args.threadId,
147
+ messages: args.messages,
148
+ contextOptions: args.contextOptions,
149
+ });
150
+ return context;
151
+ },
152
+ });
153
+
74
154
  const testApi: ApiFromModules<{
75
155
  fns: {
76
156
  createAndGenerate: typeof createAndGenerate;
77
157
  createThread: typeof createThread;
78
158
  testQuery: typeof testQuery;
159
+ continueThreadAction: typeof continueThreadAction;
160
+ generateTextWithThread: typeof generateTextWithThread;
161
+ generateObjectWithThread: typeof generateObjectWithThread;
162
+ fetchContextAction: typeof fetchContextAction;
163
+ generateTextAction: typeof generateTextAction;
164
+ generateObjectAction: typeof generateObjectAction;
165
+ saveMessageMutation: typeof saveMessageMutation;
79
166
  };
80
167
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
168
  }>["fns"] = anyApi["index.test"] as any;
@@ -90,7 +177,7 @@ describe("Agent thick client", () => {
90
177
  const t = initConvexTest(schema);
91
178
  const result = await t.action(testApi.createAndGenerate, {});
92
179
  expect(result).toBeDefined();
93
- expect(result).toMatch("This is a sample response");
180
+ expect(result).toMatch("Hello");
94
181
  });
95
182
  });
96
183
 
@@ -164,12 +251,14 @@ function mockModel(): LanguageModelV1 {
164
251
  return new MockLanguageModelV1({
165
252
  provider: "mock",
166
253
  modelId: "mock",
254
+ defaultObjectGenerationMode: "json",
255
+ // supportsStructuredOutputs: true,
167
256
  doGenerate: async ({ prompt }) => ({
168
257
  finishReason: "stop",
169
258
  usage: { completionTokens: 10, promptTokens: 3 },
170
259
  logprobs: undefined,
171
260
  rawCall: { rawPrompt: null, rawSettings: {} },
172
- text: `This is a sample response to ${JSON.stringify(prompt)}`,
261
+ text: JSON.stringify({ prompt }),
173
262
  }),
174
263
  doStream: async ({ prompt }) => ({
175
264
  stream: simulateReadableStream({
@@ -192,3 +281,177 @@ function mockModel(): LanguageModelV1 {
192
281
  }),
193
282
  });
194
283
  }
284
+
285
+ describe("Agent option variations and normal behavior", () => {
286
+ test("Agent can be constructed with minimal options", () => {
287
+ const a = new Agent(components.agent, { chat: mockModel() });
288
+ expect(a).toBeInstanceOf(Agent);
289
+ });
290
+
291
+ test("Agent can be constructed with all options", () => {
292
+ const a = new Agent(components.agent, {
293
+ name: "full",
294
+ chat: mockModel(),
295
+ instructions: "Test instructions",
296
+ contextOptions: { recentMessages: 5 },
297
+ storageOptions: { saveMessages: "all" },
298
+ maxSteps: 2,
299
+ maxRetries: 1,
300
+ usageHandler: async () => {},
301
+ rawRequestResponseHandler: async () => {},
302
+ });
303
+ expect(a.options.name).toBe("full");
304
+ });
305
+ });
306
+
307
+ describe("Agent thread management", () => {
308
+ test("createThread returns threadId (mutation context)", async () => {
309
+ const t = initConvexTest(schema);
310
+ const { threadId } = await t.run(async (ctx) =>
311
+ agent.createThread(ctx, { userId: "2" })
312
+ );
313
+ expect(threadId).toBeTypeOf("string");
314
+ });
315
+
316
+ test("continueThread returns thread object", async () => {
317
+ const t = initConvexTest(schema);
318
+ const { threadId } = await t.run(async (ctx) =>
319
+ agent.createThread(ctx, { userId: "3" })
320
+ );
321
+ const result = await t.action(testApi.continueThreadAction, {
322
+ threadId,
323
+ userId: "3",
324
+ });
325
+ expect(result.threadId).toBe(threadId);
326
+ });
327
+ });
328
+
329
+ describe("Agent message operations", () => {
330
+ test("saveMessage and saveMessages store messages", async () => {
331
+ const t = initConvexTest(schema);
332
+ const { threadId } = await t.run(async (ctx) =>
333
+ agent.createThread(ctx, { userId: "4" })
334
+ );
335
+ const { messageId } = await t.run(async (ctx) =>
336
+ agent.saveMessage(ctx, {
337
+ threadId,
338
+ userId: "4",
339
+ message: { role: "user", content: "Hello" },
340
+ })
341
+ );
342
+ expect(messageId).toBeTypeOf("string");
343
+
344
+ const { lastMessageId, messages } = await t.run(async (ctx) =>
345
+ agent.saveMessages(ctx, {
346
+ threadId,
347
+ userId: "4",
348
+ messages: [
349
+ { role: "user", content: "Hi" },
350
+ { role: "assistant", content: "Hello!" },
351
+ ],
352
+ })
353
+ );
354
+ expect(messages.length).toBe(2);
355
+ expect(lastMessageId).toBe(messages[1]._id);
356
+ });
357
+ });
358
+
359
+ describe("Agent text/object generation", () => {
360
+ test("generateText with custom context and storage options", async () => {
361
+ const t = initConvexTest(schema);
362
+ const { threadId } = await t.run(async (ctx) =>
363
+ agent.createThread(ctx, { userId: "5" })
364
+ );
365
+ const result = await t.action(testApi.generateTextWithThread, {
366
+ threadId,
367
+ userId: "5",
368
+ messages: [{ role: "user", content: "Test" }],
369
+ contextOptions: { recentMessages: 1 },
370
+ storageOptions: { saveMessages: "all" },
371
+ });
372
+ expect(result.text).toMatch(/Test/);
373
+ });
374
+
375
+ test("generateObject returns object", async () => {
376
+ const t = initConvexTest(schema);
377
+ const { threadId } = await t.run(async (ctx) =>
378
+ agent.createThread(ctx, { userId: "6" })
379
+ );
380
+ const result = await t.action(testApi.generateObjectWithThread, {
381
+ threadId,
382
+ userId: "6",
383
+ prompt: "Object please",
384
+ });
385
+ expect(result.object).toBeDefined();
386
+ });
387
+ });
388
+
389
+ describe("Agent-generated mutations/actions/queries", () => {
390
+ test("createThreadMutation works via t.mutation", async () => {
391
+ const t = initConvexTest(schema);
392
+ // This test is for the registered mutation, not the agent method
393
+ const result = await t.mutation(testApi.createThread, {});
394
+ expect(result.threadId).toBeTypeOf("string");
395
+ });
396
+
397
+ test("asTextAction and asObjectAction work via t.action", async () => {
398
+ const t = initConvexTest(schema);
399
+ const { threadId } = await t.run(async (ctx) =>
400
+ agent.createThread(ctx, { userId: "8" })
401
+ );
402
+ const textResult = await t.action(testApi.generateTextAction, {
403
+ userId: "8",
404
+ threadId,
405
+ messages: [{ role: "user", content: "Say hi" }],
406
+ });
407
+ expect(textResult.text).toMatch(/Say hi/);
408
+
409
+ const objResult = await t.action(testApi.generateObjectAction, {
410
+ userId: "8",
411
+ threadId,
412
+ messages: [{ role: "user", content: "Give object" }],
413
+ });
414
+ expect(objResult.object).toBeDefined();
415
+ });
416
+
417
+ test("asSaveMessagesMutation works via t.mutation", async () => {
418
+ const t = initConvexTest(schema);
419
+ const { threadId } = await t.run(async (ctx) =>
420
+ agent.createThread(ctx, { userId: "9" })
421
+ );
422
+ const result = await t.mutation(testApi.saveMessageMutation, {
423
+ threadId,
424
+ messages: [
425
+ {
426
+ message: { role: "user", content: "Saved via mutation" },
427
+ // add more metadata fields as needed
428
+ },
429
+ ],
430
+ });
431
+ expect(result.lastMessageId).toBeDefined();
432
+ expect(result.messageIds.length).toBe(1);
433
+ });
434
+ });
435
+
436
+ describe("Agent context and search options", () => {
437
+ test("fetchContextMessages returns context messages", async () => {
438
+ const t = initConvexTest(schema);
439
+ const { threadId } = await t.run(async (ctx) =>
440
+ agent.createThread(ctx, { userId: "10" })
441
+ );
442
+ await t.run(async (ctx) =>
443
+ agent.saveMessage(ctx, {
444
+ threadId,
445
+ userId: "10",
446
+ message: { role: "user", content: "Context test" },
447
+ })
448
+ );
449
+ const context = await t.action(testApi.fetchContextAction, {
450
+ userId: "10",
451
+ threadId,
452
+ messages: [{ role: "user", content: "Context test" }],
453
+ contextOptions: { recentMessages: 1 },
454
+ });
455
+ expect(context.length).toBeGreaterThan(0);
456
+ });
457
+ });
@@ -287,7 +287,17 @@ describe("mergeDeltas", () => {
287
287
  expect(messages1.map((m) => omit(m, ["_creationTime"]))).toEqual(
288
288
  messages2.map((m) => omit(m, ["_creationTime"]))
289
289
  );
290
- expect(streams1).toEqual(streams2);
290
+ expect(
291
+ streams1.map((s) => ({
292
+ ...s,
293
+ messages: s.messages.map((m) => omit(m, ["_creationTime"])),
294
+ }))
295
+ ).toEqual(
296
+ streams2.map((s) => ({
297
+ ...s,
298
+ messages: s.messages.map((m) => omit(m, ["_creationTime"])),
299
+ }))
300
+ );
291
301
  expect(changed1).toBe(changed2);
292
302
  // Inputs should remain unchanged
293
303
  expect(streamMessages).toEqual([makeStreamMessage(streamId, 8, 0)]);
@@ -279,7 +279,6 @@ export function usePaginatedQuery<Query extends PaginatedQueryReference>(
279
279
  allItems.push(...currResult.page);
280
280
  }
281
281
  return [allItems, currResult];
282
- // eslint-disable-next-line react-hooks/exhaustive-deps
283
282
  }, [
284
283
  resultsObject,
285
284
  currState.pageKeys,