@convex-dev/agent 0.0.1-alpha.1 → 0.0.1-alpha.2

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.
Files changed (54) hide show
  1. package/README.md +297 -6
  2. package/dist/commonjs/client/index.d.ts +480 -56
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +154 -78
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/client/types.d.ts +3 -0
  7. package/dist/commonjs/client/types.d.ts.map +1 -1
  8. package/dist/commonjs/component/messages.d.ts +10 -9
  9. package/dist/commonjs/component/messages.d.ts.map +1 -1
  10. package/dist/commonjs/component/messages.js +84 -41
  11. package/dist/commonjs/component/messages.js.map +1 -1
  12. package/dist/commonjs/component/schema.d.ts +10 -6
  13. package/dist/commonjs/component/schema.d.ts.map +1 -1
  14. package/dist/commonjs/component/schema.js +9 -2
  15. package/dist/commonjs/component/schema.js.map +1 -1
  16. package/dist/commonjs/mapping.d.ts +6 -1
  17. package/dist/commonjs/mapping.d.ts.map +1 -1
  18. package/dist/commonjs/mapping.js +25 -0
  19. package/dist/commonjs/mapping.js.map +1 -1
  20. package/dist/commonjs/validators.d.ts +1375 -0
  21. package/dist/commonjs/validators.d.ts.map +1 -1
  22. package/dist/commonjs/validators.js +27 -0
  23. package/dist/commonjs/validators.js.map +1 -1
  24. package/dist/esm/client/index.d.ts +480 -56
  25. package/dist/esm/client/index.d.ts.map +1 -1
  26. package/dist/esm/client/index.js +154 -78
  27. package/dist/esm/client/index.js.map +1 -1
  28. package/dist/esm/client/types.d.ts +3 -0
  29. package/dist/esm/client/types.d.ts.map +1 -1
  30. package/dist/esm/component/messages.d.ts +10 -9
  31. package/dist/esm/component/messages.d.ts.map +1 -1
  32. package/dist/esm/component/messages.js +84 -41
  33. package/dist/esm/component/messages.js.map +1 -1
  34. package/dist/esm/component/schema.d.ts +10 -6
  35. package/dist/esm/component/schema.d.ts.map +1 -1
  36. package/dist/esm/component/schema.js +9 -2
  37. package/dist/esm/component/schema.js.map +1 -1
  38. package/dist/esm/mapping.d.ts +6 -1
  39. package/dist/esm/mapping.d.ts.map +1 -1
  40. package/dist/esm/mapping.js +25 -0
  41. package/dist/esm/mapping.js.map +1 -1
  42. package/dist/esm/validators.d.ts +1375 -0
  43. package/dist/esm/validators.d.ts.map +1 -1
  44. package/dist/esm/validators.js +27 -0
  45. package/dist/esm/validators.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/client/index.ts +290 -188
  48. package/src/client/types.ts +4 -0
  49. package/src/component/_generated/api.d.ts +7 -6
  50. package/src/component/messages.ts +106 -58
  51. package/src/component/schema.ts +9 -2
  52. package/src/mapping.ts +46 -11
  53. package/src/validators.test.ts +9 -0
  54. package/src/validators.ts +32 -0
@@ -7,6 +7,10 @@ import {
7
7
  GenericActionCtx,
8
8
  } from "convex/server";
9
9
  import { GenericId } from "convex/values";
10
+ import type { Doc } from "../component/_generated/dataModel";
11
+
12
+ export type ChatDoc = OpaqueIds<Doc<"chats">>;
13
+ export type MessageDoc = OpaqueIds<Doc<"messages">>;
10
14
 
11
15
  /* Type utils follow */
12
16
  export type RunQueryCtx = {
@@ -40,7 +40,7 @@ export type Mounts = {
40
40
  "public",
41
41
  {
42
42
  agentName?: string;
43
- chatId: string;
43
+ chatId?: string;
44
44
  failPendingSteps?: boolean;
45
45
  messages: Array<{
46
46
  fileId?: string;
@@ -147,13 +147,14 @@ export type Mounts = {
147
147
  parentMessageId?: string;
148
148
  pending?: boolean;
149
149
  stepId?: string;
150
+ userId?: string;
150
151
  },
151
152
  {
152
153
  messages: Array<{
153
154
  _creationTime: number;
154
155
  _id: string;
155
156
  agentName?: string;
156
- chatId: string;
157
+ chatId?: string;
157
158
  embeddingId?:
158
159
  | string
159
160
  | string
@@ -277,7 +278,7 @@ export type Mounts = {
277
278
  _creationTime: number;
278
279
  _id: string;
279
280
  agentName?: string;
280
- chatId: string;
281
+ chatId?: string;
281
282
  embeddingId?:
282
283
  | string
283
284
  | string
@@ -1021,7 +1022,7 @@ export type Mounts = {
1021
1022
  _creationTime: number;
1022
1023
  _id: string;
1023
1024
  agentName?: string;
1024
- chatId: string;
1025
+ chatId?: string;
1025
1026
  embeddingId?:
1026
1027
  | string
1027
1028
  | string
@@ -1208,7 +1209,7 @@ export type Mounts = {
1208
1209
  _creationTime: number;
1209
1210
  _id: string;
1210
1211
  agentName?: string;
1211
- chatId: string;
1212
+ chatId?: string;
1212
1213
  embeddingId?:
1213
1214
  | string
1214
1215
  | string
@@ -1337,7 +1338,7 @@ export type Mounts = {
1337
1338
  _creationTime: number;
1338
1339
  _id: string;
1339
1340
  agentName?: string;
1340
- chatId: string;
1341
+ chatId?: string;
1341
1342
  embeddingId?:
1342
1343
  | string
1343
1344
  | string
@@ -2,19 +2,14 @@ import { assert, omit, pick } from "convex-helpers";
2
2
  import { paginator } from "convex-helpers/server/pagination";
3
3
  import { mergedStream, stream } from "convex-helpers/server/stream";
4
4
  import { nullable, partial } from "convex-helpers/validators";
5
- import { Infer, ObjectType } from "convex/values";
5
+ import { ObjectType } from "convex/values";
6
6
  import { DEFAULT_MESSAGE_RANGE, extractText, isTool } from "../shared.js";
7
7
  import {
8
- Message,
9
- MessageWithFileAndId,
10
- vAssistantMessage,
11
8
  vChatStatus,
12
9
  vMessageStatus,
13
10
  vMessageWithFileAndId,
14
11
  vSearchOptions,
15
- vStep,
16
12
  vStepWithMessagesWithFileAndId,
17
- vToolMessage,
18
13
  } from "../validators.js";
19
14
  import { api, internal } from "./_generated/api.js";
20
15
  import { Doc, Id } from "./_generated/dataModel.js";
@@ -63,7 +58,7 @@ export const getChatsByUserId = query({
63
58
  .gte("order", args.offset ?? 0)
64
59
  )
65
60
  );
66
- const chats = await mergedStream(streams, ["order", "stepOrder"]).paginate({
61
+ const chats = await mergedStream(streams, ["order"]).paginate({
67
62
  numItems: args.limit ?? 100,
68
63
  cursor: args.cursor ?? null,
69
64
  });
@@ -377,7 +372,8 @@ export const messageStatuses = vMessageDoc.fields.status.members.map(
377
372
  );
378
373
 
379
374
  const addMessagesArgs = {
380
- chatId: v.id("chats"),
375
+ userId: v.optional(v.string()),
376
+ chatId: v.optional(v.id("chats")),
381
377
  stepId: v.optional(v.id("steps")),
382
378
  parentMessageId: v.optional(v.id("messages")),
383
379
  messages: v.array(vMessageWithFileAndId),
@@ -398,15 +394,22 @@ async function addMessagesHandler(
398
394
  ctx: MutationCtx,
399
395
  args: ObjectType<typeof addMessagesArgs>
400
396
  ) {
401
- const chat = await ctx.db.get(args.chatId);
402
- assert(chat, `Chat ${args.chatId} not found`);
397
+ let userId = args.userId;
398
+ const chatId = args.chatId;
399
+ if (!userId && args.chatId) {
400
+ const chat = await ctx.db.get(args.chatId);
401
+ assert(chat, `Chat ${args.chatId} not found`);
402
+ userId = chat._id;
403
+ }
403
404
  const { failPendingSteps, pending, messages, parentMessageId, ...rest } =
404
405
  args;
405
- if (failPendingSteps) {
406
+ const parent = parentMessageId && (await ctx.db.get(parentMessageId));
407
+ if (failPendingSteps && parent?.status !== "pending") {
408
+ assert(args.chatId, "chatId is required to fail pending steps");
406
409
  const pendingMessages = await ctx.db
407
410
  .query("messages")
408
411
  .withIndex("chatId_status_tool_order_stepOrder", (q) =>
409
- q.eq("chatId", args.chatId).eq("status", "pending")
412
+ q.eq("chatId", chatId).eq("status", "pending")
410
413
  )
411
414
  .collect();
412
415
  await Promise.all(
@@ -415,14 +418,14 @@ async function addMessagesHandler(
415
418
  )
416
419
  );
417
420
  }
418
- let order: number | undefined;
419
- const maxMessage = await getMaxMessage(ctx, args.chatId);
421
+ let threadId = parentMessageId;
422
+ const maxMessage = await getMaxMessage(ctx, chatId, userId);
420
423
  // If the previous message isn't our parent, we make a new thread.
421
- const threadId =
424
+ threadId =
422
425
  parentMessageId && maxMessage?._id === parentMessageId
423
426
  ? maxMessage.threadId ?? parentMessageId
424
427
  : parentMessageId;
425
- order = maxMessage?.order ?? -1;
428
+ let order = maxMessage?.order ?? -1;
426
429
  const toReturn: Doc<"messages">[] = [];
427
430
  if (messages.length > 0) {
428
431
  for (const { message, fileId, id } of messages) {
@@ -434,7 +437,7 @@ async function addMessagesHandler(
434
437
  const messageId = await ctx.db.insert("messages", {
435
438
  ...rest,
436
439
  threadId,
437
- userId: chat.userId,
440
+ userId,
438
441
  message,
439
442
  id,
440
443
  order,
@@ -449,18 +452,37 @@ async function addMessagesHandler(
449
452
  return { messages: toReturn };
450
453
  }
451
454
 
452
- async function getMaxMessage(ctx: QueryCtx, chatId: Id<"chats">) {
453
- return mergedStream(
454
- ["success" as const, "pending" as const].map((status) =>
455
- stream(ctx.db, schema)
456
- .query("messages")
457
- .withIndex("chatId_status_tool_order_stepOrder", (q) =>
458
- q.eq("chatId", chatId).eq("status", status).eq("tool", false)
459
- )
460
- .order("desc")
461
- ),
462
- ["order", "stepOrder"]
463
- ).first();
455
+ async function getMaxMessage(
456
+ ctx: QueryCtx,
457
+ chatId: Id<"chats"> | undefined,
458
+ userId: string | undefined
459
+ ) {
460
+ assert(chatId || userId, "One of chatId or userId is required");
461
+ if (chatId) {
462
+ return mergedStream(
463
+ ["success" as const, "pending" as const].map((status) =>
464
+ stream(ctx.db, schema)
465
+ .query("messages")
466
+ .withIndex("chatId_status_tool_order_stepOrder", (q) =>
467
+ q.eq("chatId", chatId).eq("status", status).eq("tool", false)
468
+ )
469
+ .order("desc")
470
+ ),
471
+ ["order", "stepOrder"]
472
+ ).first();
473
+ } else {
474
+ return mergedStream(
475
+ ["success" as const, "pending" as const].map((status) =>
476
+ stream(ctx.db, schema)
477
+ .query("messages")
478
+ .withIndex("userId_status_tool_order_stepOrder", (q) =>
479
+ q.eq("userId", userId).eq("status", status).eq("tool", false)
480
+ )
481
+ .order("desc")
482
+ ),
483
+ ["order", "stepOrder"]
484
+ ).first();
485
+ }
464
486
  }
465
487
 
466
488
  const addStepsArgs = {
@@ -518,7 +540,8 @@ async function addStepsHandler(
518
540
  pending: step.finishReason === "stop" ? false : true,
519
541
  failPendingSteps: false,
520
542
  });
521
- if (step.finishReason === "stop") {
543
+ // We don't commit if the parent is still pending.
544
+ if (step.finishReason === "stop" && parentMessage.status === "success") {
522
545
  await commitMessageHandler(ctx, { messageId: args.messageId });
523
546
  }
524
547
  steps.push((await ctx.db.get(stepId))!);
@@ -585,6 +608,7 @@ async function commitMessageHandler(
585
608
  ).collect();
586
609
  for (const message of messages) {
587
610
  await ctx.db.patch(message._id, { status: "success" });
611
+ // TODO: recursively commit steps & messages that might depend on this one.
588
612
  }
589
613
  }
590
614
 
@@ -727,18 +751,19 @@ export const _fetchVectorMessages = internalQuery({
727
751
  handler: async (ctx, args): Promise<Doc<"messages">[]> => {
728
752
  const parent =
729
753
  args.parentMessageId && (await ctx.db.get(args.parentMessageId));
754
+ const { userId, chatId } = args;
755
+ assert(userId || chatId, "Specify userId or chatId to search");
730
756
  let messages = (
731
757
  await Promise.all(
732
758
  args.vectorIds.map((embeddingId) =>
733
759
  ctx.db
734
760
  .query("messages")
735
761
  .withIndex("embeddingId", (q) => q.eq("embeddingId", embeddingId))
736
- .filter(
737
- (q) =>
738
- args.userId
739
- ? q.eq("userId", args.userId)
740
- : // eslint-disable-next-line @typescript-eslint/no-explicit-any
741
- q.eq("chatId", args.chatId as any) // not sure why it's failing...
762
+ .filter((q) =>
763
+ userId
764
+ ? q.eq("userId", userId)
765
+ : // eslint-disable-next-line @typescript-eslint/no-explicit-any
766
+ q.eq("chatId", chatId as any)
742
767
  )
743
768
  .first()
744
769
  )
@@ -752,48 +777,71 @@ export const _fetchVectorMessages = internalQuery({
752
777
  messages.sort((a, b) => a.order! - b.order!);
753
778
  messages = messages.slice(0, args.limit);
754
779
  // Fetch the surrounding messages
755
- const included: Record<Id<"chats">, Set<number>> = {};
780
+ if (!chatId) {
781
+ return messages.sort((a, b) => a.order - b.order);
782
+ }
783
+ const included: Record<string, Set<number>> = {};
756
784
  for (const m of messages) {
757
- if (!included[m.chatId]) {
758
- included[m.chatId] = new Set();
785
+ const searchId = m.chatId ?? m.userId!;
786
+ if (!included[searchId]) {
787
+ included[searchId] = new Set();
759
788
  }
760
- included[m.chatId].add(m.order!);
789
+ included[searchId].add(m.order!);
761
790
  }
762
- const ranges: Record<Id<"chats">, Doc<"messages">[]> = {};
791
+ const ranges: Record<string, Doc<"messages">[]> = {};
763
792
  const { before, after } = args.messageRange;
764
793
  for (const m of messages) {
794
+ const searchId = m.chatId ?? m.userId!;
765
795
  const order = m.order!;
766
796
  let earliest = order - before;
767
797
  let latest = order + after;
768
798
  for (; earliest <= latest; earliest++) {
769
- if (!included[m.chatId].has(earliest)) {
799
+ if (!included[searchId].has(earliest)) {
770
800
  break;
771
801
  }
772
802
  }
773
803
  for (; latest >= earliest; latest--) {
774
- if (!included[m.chatId].has(latest)) {
804
+ if (!included[searchId].has(latest)) {
775
805
  break;
776
806
  }
777
807
  }
778
808
  for (let i = earliest; i <= latest; i++) {
779
- included[m.chatId].add(i);
809
+ included[searchId].add(i);
780
810
  }
781
811
  if (earliest !== latest) {
782
- const surrounding = await ctx.db
783
- .query("messages")
784
- .withIndex("chatId_status_tool_order_stepOrder", (q) =>
785
- q
786
- .eq("chatId", m.chatId)
787
- .eq("status", "success")
788
- .eq("tool", false)
789
- .gt("order", earliest)
790
- .lt("order", latest)
791
- )
792
- .collect();
793
- if (!ranges[m.chatId]) {
794
- ranges[m.chatId] = [];
812
+ if (m.chatId) {
813
+ const surrounding = await ctx.db
814
+ .query("messages")
815
+ .withIndex("chatId_status_tool_order_stepOrder", (q) =>
816
+ q
817
+ .eq("chatId", m.chatId)
818
+ .eq("status", "success")
819
+ .eq("tool", false)
820
+ .gt("order", earliest)
821
+ .lt("order", latest)
822
+ )
823
+ .collect();
824
+ if (!ranges[searchId]) {
825
+ ranges[searchId] = [];
826
+ }
827
+ ranges[searchId].push(...surrounding);
828
+ } else {
829
+ const surrounding = await ctx.db
830
+ .query("messages")
831
+ .withIndex("userId_status_tool_order_stepOrder", (q) =>
832
+ q
833
+ .eq("userId", m.userId!)
834
+ .eq("status", "success")
835
+ .eq("tool", false)
836
+ .gt("order", earliest)
837
+ .lt("order", latest)
838
+ )
839
+ .collect();
840
+ if (!ranges[searchId]) {
841
+ ranges[searchId] = [];
842
+ }
843
+ ranges[searchId].push(...surrounding);
795
844
  }
796
- ranges[m.chatId].push(...surrounding);
797
845
  }
798
846
  }
799
847
  return Object.values(ranges)
@@ -22,7 +22,7 @@ export const schema = defineSchema({
22
22
  messages: defineTable({
23
23
  id: v.optional(v.string()), // external id, e.g. from Vercel AI SDK
24
24
  userId: v.optional(v.string()), // useful for future indexes (text search)
25
- chatId: v.id("chats"),
25
+ chatId: v.optional(v.id("chats")),
26
26
  threadId: v.optional(v.id("messages")),
27
27
  stepId: v.optional(v.id("steps")),
28
28
  agentName: v.optional(v.string()),
@@ -33,7 +33,7 @@ export const schema = defineSchema({
33
33
  // TODO: add sub-messages back in? or be able to skip them?
34
34
  tool: v.boolean(),
35
35
  // Repeats until a non-tool message.
36
- // Set when the message is finished
36
+ // Unset if it's not in a chat.
37
37
  order: v.number(),
38
38
  stepOrder: v.optional(v.number()),
39
39
  fileId: v.optional(v.id("files")),
@@ -48,6 +48,13 @@ export const schema = defineSchema({
48
48
  "order",
49
49
  "stepOrder",
50
50
  ])
51
+ .index("userId_status_tool_order_stepOrder", [
52
+ "userId",
53
+ "status",
54
+ "tool",
55
+ "order",
56
+ "stepOrder",
57
+ ])
51
58
  // Allows finding all threaded messages in order
52
59
  // Allows finding all failed messages to evaluate
53
60
  // .index("status_threadId_order_stepOrder", [
package/src/mapping.ts CHANGED
@@ -1,16 +1,20 @@
1
- import type {
2
- AssistantContent,
3
- CoreAssistantMessage,
4
- CoreMessage,
5
- CoreToolMessage,
6
- DataContent,
7
- GenerateTextResult,
8
- StepResult,
9
- ToolContent,
10
- ToolSet,
11
- UserContent,
1
+ import {
2
+ convertToCoreMessages,
3
+ coreMessageSchema,
4
+ type AssistantContent,
5
+ type CoreAssistantMessage,
6
+ type CoreMessage,
7
+ type CoreToolMessage,
8
+ type DataContent,
9
+ type GenerateTextResult,
10
+ type StepResult,
11
+ type ToolContent,
12
+ type ToolSet,
13
+ type Message as UIMessage,
14
+ type UserContent,
12
15
  } from "ai";
13
16
  import { MessageWithFileAndId, Step } from "./validators";
17
+ import { assert } from "convex-helpers";
14
18
 
15
19
  export type SerializeUrlsAndUint8Arrays<T> = T extends URL
16
20
  ? string
@@ -158,3 +162,34 @@ function deserializeUrl(urlOrString: string | ArrayBuffer): URL | DataContent {
158
162
  }
159
163
  return urlOrString;
160
164
  }
165
+
166
+ export function promptOrMessagesToCoreMessages(args: {
167
+ system?: string;
168
+ prompt?: string;
169
+ messages?: CoreMessage[] | Omit<UIMessage, "id">[];
170
+ }): CoreMessage[] {
171
+ const messages: CoreMessage[] = [];
172
+ if (args.system) {
173
+ messages.push({ role: "system", content: args.system });
174
+ }
175
+ if (!args.messages) {
176
+ assert(args.prompt, "messages or prompt is required");
177
+ messages.push({ role: "user", content: args.prompt });
178
+ } else if (
179
+ args.messages.some(
180
+ (m) =>
181
+ typeof m === "object" &&
182
+ m !== null &&
183
+ (m.role === "data" || // UI-only role
184
+ "toolInvocations" in m || // UI-specific field
185
+ "parts" in m || // UI-specific field
186
+ "experimental_attachments" in m)
187
+ )
188
+ ) {
189
+ messages.push(...convertToCoreMessages(args.messages as UIMessage[]));
190
+ } else {
191
+ messages.push(...coreMessageSchema.array().parse(args.messages));
192
+ }
193
+ assert(messages.length > 0, "Messages must contain at least one message");
194
+ return messages;
195
+ }
@@ -3,11 +3,13 @@ import { expectTypeOf, test } from "vitest";
3
3
  import {
4
4
  vAssistantContent,
5
5
  vAssistantMessage,
6
+ vContextOptions,
6
7
  vFilePart,
7
8
  vImagePart,
8
9
  vMessage,
9
10
  vReasoningPart,
10
11
  vRedactedReasoningPart,
12
+ vStorageOptions,
11
13
  vSystemMessage,
12
14
  vTextPart,
13
15
  vToolCallPart,
@@ -31,6 +33,7 @@ import {
31
33
  UserContent,
32
34
  } from "ai";
33
35
  import { SerializeUrlsAndUint8Arrays } from "./mapping";
36
+ import { ContextOptions, StorageOptions } from "./client";
34
37
 
35
38
  // type assertion
36
39
  type OurUserContent = SerializeUrlsAndUint8Arrays<UserContent>;
@@ -98,4 +101,10 @@ type OurMessage = SerializeUrlsAndUint8Arrays<CoreMessage>;
98
101
  expectTypeOf<OurMessage>().toExtend<Infer<typeof vMessage>>();
99
102
  expectTypeOf<Infer<typeof vMessage>>().toExtend<OurMessage>();
100
103
 
104
+ expectTypeOf<Infer<typeof vContextOptions>>().toExtend<ContextOptions>();
105
+ expectTypeOf<ContextOptions>().toExtend<Infer<typeof vContextOptions>>();
106
+
107
+ expectTypeOf<Infer<typeof vStorageOptions>>().toExtend<StorageOptions>();
108
+ expectTypeOf<StorageOptions>().toExtend<Infer<typeof vStorageOptions>>();
109
+
101
110
  test("noop", () => { });
package/src/validators.ts CHANGED
@@ -256,3 +256,35 @@ export const vSearchOptions = v.object({
256
256
  messageRange: v.optional(v.object({ before: v.number(), after: v.number() })),
257
257
  });
258
258
  export type SearchOptions = Infer<typeof vSearchOptions>;
259
+
260
+ export const vContextOptionsSearchOptions = v.object({
261
+ limit: v.number(),
262
+ textSearch: v.optional(v.boolean()),
263
+ vectorSearch: v.optional(v.boolean()),
264
+ messageRange: v.optional(v.object({ before: v.number(), after: v.number() })),
265
+ });
266
+
267
+ export const vContextOptions = v.object({
268
+ includeToolCalls: v.optional(v.boolean()),
269
+ recentMessages: v.optional(v.number()),
270
+ searchOptions: v.optional(vContextOptionsSearchOptions),
271
+ searchOtherChats: v.optional(v.boolean()),
272
+ });
273
+
274
+ export const vStorageOptions = v.object({
275
+ saveAllInputMessages: v.optional(v.boolean()),
276
+ saveAllOutputMessages: v.optional(v.boolean()),
277
+ });
278
+
279
+ export const vChatArgs = v.object({
280
+ maxSteps: v.optional(v.number()),
281
+ prompt: v.optional(v.string()),
282
+ messages: v.optional(v.array(vMessage)),
283
+ });
284
+
285
+ export const vObjectArgs = v.object({
286
+ output: v.optional(v.any()),
287
+ mode: v.optional(v.literal("json")),
288
+ prompt: v.optional(v.string()),
289
+ messages: v.optional(v.array(vMessage)),
290
+ });