@convex-dev/agent 0.0.16-alpha.0 → 0.0.17-alpha.0

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 (73) hide show
  1. package/README.md +92 -71
  2. package/dist/commonjs/client/index.d.ts +1012 -920
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +188 -31
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/client/playground.d.ts +251 -116
  7. package/dist/commonjs/client/playground.d.ts.map +1 -1
  8. package/dist/commonjs/client/playground.js +33 -11
  9. package/dist/commonjs/client/playground.js.map +1 -1
  10. package/dist/commonjs/component/messages.d.ts +683 -571
  11. package/dist/commonjs/component/messages.d.ts.map +1 -1
  12. package/dist/commonjs/component/messages.js +56 -33
  13. package/dist/commonjs/component/messages.js.map +1 -1
  14. package/dist/commonjs/component/schema.d.ts +1080 -1324
  15. package/dist/commonjs/component/schema.d.ts.map +1 -1
  16. package/dist/commonjs/component/schema.js +20 -14
  17. package/dist/commonjs/component/schema.js.map +1 -1
  18. package/dist/commonjs/component/vector/index.d.ts.map +1 -1
  19. package/dist/commonjs/component/vector/index.js +4 -6
  20. package/dist/commonjs/component/vector/index.js.map +1 -1
  21. package/dist/commonjs/component/vector/tables.d.ts +10 -10
  22. package/dist/commonjs/component/vector/tables.d.ts.map +1 -1
  23. package/dist/commonjs/component/vector/tables.js.map +1 -1
  24. package/dist/commonjs/mapping.d.ts +7 -1
  25. package/dist/commonjs/mapping.d.ts.map +1 -1
  26. package/dist/commonjs/mapping.js +39 -18
  27. package/dist/commonjs/mapping.js.map +1 -1
  28. package/dist/commonjs/validators.d.ts +1669 -2336
  29. package/dist/commonjs/validators.d.ts.map +1 -1
  30. package/dist/commonjs/validators.js +35 -25
  31. package/dist/commonjs/validators.js.map +1 -1
  32. package/dist/esm/client/index.d.ts +1012 -920
  33. package/dist/esm/client/index.d.ts.map +1 -1
  34. package/dist/esm/client/index.js +188 -31
  35. package/dist/esm/client/index.js.map +1 -1
  36. package/dist/esm/client/playground.d.ts +251 -116
  37. package/dist/esm/client/playground.d.ts.map +1 -1
  38. package/dist/esm/client/playground.js +33 -11
  39. package/dist/esm/client/playground.js.map +1 -1
  40. package/dist/esm/component/messages.d.ts +683 -571
  41. package/dist/esm/component/messages.d.ts.map +1 -1
  42. package/dist/esm/component/messages.js +56 -33
  43. package/dist/esm/component/messages.js.map +1 -1
  44. package/dist/esm/component/schema.d.ts +1080 -1324
  45. package/dist/esm/component/schema.d.ts.map +1 -1
  46. package/dist/esm/component/schema.js +20 -14
  47. package/dist/esm/component/schema.js.map +1 -1
  48. package/dist/esm/component/vector/index.d.ts.map +1 -1
  49. package/dist/esm/component/vector/index.js +4 -6
  50. package/dist/esm/component/vector/index.js.map +1 -1
  51. package/dist/esm/component/vector/tables.d.ts +10 -10
  52. package/dist/esm/component/vector/tables.d.ts.map +1 -1
  53. package/dist/esm/component/vector/tables.js.map +1 -1
  54. package/dist/esm/mapping.d.ts +7 -1
  55. package/dist/esm/mapping.d.ts.map +1 -1
  56. package/dist/esm/mapping.js +39 -18
  57. package/dist/esm/mapping.js.map +1 -1
  58. package/dist/esm/validators.d.ts +1669 -2336
  59. package/dist/esm/validators.d.ts.map +1 -1
  60. package/dist/esm/validators.js +35 -25
  61. package/dist/esm/validators.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/client/index.test.ts +68 -0
  64. package/src/client/index.ts +219 -35
  65. package/src/client/playground.ts +49 -23
  66. package/src/component/_generated/api.d.ts +311 -323
  67. package/src/component/messages.test.ts +23 -0
  68. package/src/component/messages.ts +75 -42
  69. package/src/component/schema.ts +26 -13
  70. package/src/component/vector/index.ts +5 -6
  71. package/src/component/vector/tables.ts +15 -15
  72. package/src/mapping.ts +65 -32
  73. package/src/validators.ts +47 -27
@@ -15,7 +15,9 @@ import type {
15
15
  ToolChoice,
16
16
  ToolExecutionOptions,
17
17
  ToolSet,
18
+ UIMessage,
18
19
  } from "ai";
20
+ import type { ToolInvocationUIPart } from "@ai-sdk/ui-utils";
19
21
  import {
20
22
  generateObject,
21
23
  generateText,
@@ -40,6 +42,7 @@ import {
40
42
  serializeNewMessagesInStep,
41
43
  serializeObjectResult,
42
44
  serializeStep,
45
+ toUIFilePart,
43
46
  } from "../mapping.js";
44
47
  import {
45
48
  DEFAULT_MESSAGE_RANGE,
@@ -49,16 +52,18 @@ import {
49
52
  } from "../shared.js";
50
53
  import {
51
54
  type CallSettings,
52
- MessageWithMetadata,
55
+ type MessageWithMetadata as InnerMessageWithMetadata,
53
56
  type ProviderMetadata,
54
57
  type ProviderOptions,
55
58
  type SearchOptions,
56
59
  type Usage,
60
+ vFileWithStringId,
57
61
  vSafeObjectArgs,
58
62
  vTextArgs,
59
63
  vThreadStatus,
60
64
  } from "../validators.js";
61
65
  import type {
66
+ OpaqueIds,
62
67
  RunActionCtx,
63
68
  RunMutationCtx,
64
69
  RunQueryCtx,
@@ -69,39 +74,18 @@ import schema from "../component/schema.js";
69
74
  export { extractText, isTool };
70
75
  export type { Usage, ProviderMetadata };
71
76
  export {
77
+ paginationResultValidator,
78
+ vContextOptions,
72
79
  vUsage,
73
80
  vProviderMetadata,
74
81
  vUserMessage,
75
82
  vAssistantMessage,
76
83
  vToolMessage,
84
+ vStorageOptions,
77
85
  vSystemMessage,
78
86
  vMessage,
79
87
  } from "../validators.js";
80
88
 
81
- export const vThreadDoc = v.object({
82
- _id: v.string(),
83
- _creationTime: v.number(),
84
- userId: v.optional(v.string()), // Unset for anonymous
85
- title: v.optional(v.string()),
86
- summary: v.optional(v.string()),
87
- status: vThreadStatus,
88
- });
89
- export type ThreadDoc = Infer<typeof vThreadDoc>;
90
-
91
- export const vMessageDoc = v.object({
92
- _id: v.string(),
93
- _creationTime: v.number(),
94
- ...schema.tables.messages.validator.fields,
95
- // Overwrite all the types that have a v.id validator
96
- // Outside of the component, they are strings
97
- threadId: v.string(),
98
- parentMessageId: v.optional(v.string()),
99
- stepId: v.optional(v.string()),
100
- embeddingId: v.optional(v.string()),
101
- fileId: v.optional(v.string()),
102
- });
103
- export type MessageDoc = Infer<typeof vMessageDoc>;
104
-
105
89
  /**
106
90
  * Options to configure what messages are fetched as context,
107
91
  * automatically with thread.generateText, or directly via search.
@@ -188,9 +172,11 @@ export type UsageHandler = (
188
172
  }
189
173
  ) => void | Promise<void>;
190
174
 
175
+ export type AgentComponent = UseApi<Mounts>;
176
+
191
177
  export class Agent<AgentTools extends ToolSet> {
192
178
  constructor(
193
- public component: UseApi<Mounts>,
179
+ public component: AgentComponent,
194
180
  public options: {
195
181
  /**
196
182
  * The name for the agent. This will be attributed on each message
@@ -439,7 +425,7 @@ export class Agent<AgentTools extends ToolSet> {
439
425
  beforeMessageId?: string;
440
426
  contextOptions: ContextOptions | undefined;
441
427
  }
442
- ): Promise<CoreMessage[]> {
428
+ ): Promise<MessageDoc[]> {
443
429
  assert(args.userId || args.threadId, "Specify userId or threadId");
444
430
  // Fetch the latest messages from the thread
445
431
  const contextMessages: MessageDoc[] = [];
@@ -452,9 +438,7 @@ export class Agent<AgentTools extends ToolSet> {
452
438
  const searchMessages = await ctx.runAction(
453
439
  this.component.messages.searchMessages,
454
440
  {
455
- userId: args.contextOptions?.searchOtherThreads
456
- ? args.userId
457
- : undefined,
441
+ userId: opts?.searchOtherThreads ? args.userId : undefined,
458
442
  threadId: args.threadId,
459
443
  beforeMessageId: args.beforeMessageId,
460
444
  ...(await this.searchOptionsWithDefaults(opts, args.messages)),
@@ -480,13 +464,18 @@ export class Agent<AgentTools extends ToolSet> {
480
464
  statuses: ["success"],
481
465
  }
482
466
  );
483
- contextMessages.push(...page.filter((m) => !included?.has(m._id)));
467
+ contextMessages.push(
468
+ // Reverse since we fetched in descending order
469
+ ...page.filter((m) => !included?.has(m._id)).reverse()
470
+ );
484
471
  }
485
- return contextMessages
486
- .sort((a, b) =>
472
+ // Ensure we don't include tool messages without a corresponding tool call
473
+ return filterOutOrphanedToolMessages(
474
+ contextMessages.sort((a, b) =>
475
+ // Sort the raw MessageDocs by order and stepOrder
487
476
  a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order
488
477
  )
489
- .map((m) => deserializeMessage(m.message!));
478
+ );
490
479
  }
491
480
 
492
481
  /**
@@ -948,7 +937,10 @@ export class Agent<AgentTools extends ToolSet> {
948
937
  args: {
949
938
  ...rest,
950
939
  system: args.system ?? this.options.instructions,
951
- messages: [...contextMessages, ...messages],
940
+ messages: [
941
+ ...contextMessages.map((m) => deserializeMessage(m.message!)),
942
+ ...messages,
943
+ ],
952
944
  } as T,
953
945
  messageId,
954
946
  };
@@ -1322,6 +1314,34 @@ export class Agent<AgentTools extends ToolSet> {
1322
1314
  }
1323
1315
  }
1324
1316
 
1317
+ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
1318
+ const toolCallIds = new Set<string>();
1319
+ const result: MessageDoc[] = [];
1320
+ for (const doc of docs) {
1321
+ if (
1322
+ doc.message?.role === "assistant" &&
1323
+ Array.isArray(doc.message.content)
1324
+ ) {
1325
+ for (const content of doc.message.content) {
1326
+ if (content.type === "tool-call") {
1327
+ toolCallIds.add(content.toolCallId);
1328
+ }
1329
+ }
1330
+ result.push(doc);
1331
+ } else if (doc.message?.role === "tool") {
1332
+ if (doc.message.content.every((c) => toolCallIds.has(c.toolCallId))) {
1333
+ result.push(doc);
1334
+ } else {
1335
+ console.debug("Filtering out orphaned tool message", doc);
1336
+ }
1337
+ } else {
1338
+ result.push(doc);
1339
+ }
1340
+ }
1341
+ return result;
1342
+ }
1343
+
1344
+
1325
1345
  export type ToolCtx = RunActionCtx & {
1326
1346
  userId?: string;
1327
1347
  threadId?: string;
@@ -1703,3 +1723,167 @@ interface Thread<DefaultTools extends ToolSet> {
1703
1723
  StreamObjectResult<DeepPartial<T>, T, never> & ThreadOutputMetadata
1704
1724
  >;
1705
1725
  }
1726
+
1727
+ export const vThreadDoc = v.object({
1728
+ _id: v.string(),
1729
+ _creationTime: v.number(),
1730
+ userId: v.optional(v.string()), // Unset for anonymous
1731
+ title: v.optional(v.string()),
1732
+ summary: v.optional(v.string()),
1733
+ status: vThreadStatus,
1734
+ });
1735
+ export type ThreadDoc = Infer<typeof vThreadDoc>;
1736
+
1737
+ export const vMessageDoc = v.object({
1738
+ _id: v.string(),
1739
+ _creationTime: v.number(),
1740
+ ...schema.tables.messages.validator.fields,
1741
+ // Overwrite all the types that have a v.id validator
1742
+ // Outside of the component, they are strings
1743
+ threadId: v.string(),
1744
+ parentMessageId: v.optional(v.string()),
1745
+ stepId: v.optional(v.string()),
1746
+ embeddingId: v.optional(v.string()),
1747
+ files: v.optional(v.array(vFileWithStringId)),
1748
+ });
1749
+ export type MessageDoc = Infer<typeof vMessageDoc>;
1750
+
1751
+ type MessageWithMetadata = OpaqueIds<InnerMessageWithMetadata>;
1752
+
1753
+ export function toUIMessages(messages: MessageDoc[]): UIMessage[] {
1754
+ const uiMessages: UIMessage[] = [];
1755
+ let assistantMessage: UIMessage | undefined;
1756
+ for (const message of messages) {
1757
+ const coreMessage = message.message && deserializeMessage(message.message);
1758
+ const text = message.text ?? "";
1759
+ const content = coreMessage?.content;
1760
+ const nonStringContent =
1761
+ content && typeof content !== "string" ? content : [];
1762
+ if (!coreMessage) continue;
1763
+ if (coreMessage.role === "system") {
1764
+ uiMessages.push({
1765
+ id: message.id ?? message._id,
1766
+ createdAt: new Date(message._creationTime),
1767
+ role: "system",
1768
+ content: text,
1769
+ parts: [{ type: "text", text }],
1770
+ });
1771
+ } else if (coreMessage.role === "user") {
1772
+ const parts: UIMessage["parts"] = [];
1773
+ if (text) {
1774
+ parts.push({ type: "text", text });
1775
+ }
1776
+ if (message.files) {
1777
+ parts.push(...message.files.map(toUIFilePart));
1778
+ }
1779
+ uiMessages.push({
1780
+ id: message.id ?? message._id,
1781
+ createdAt: new Date(message._creationTime),
1782
+ role: "user",
1783
+ content: message.text ?? "",
1784
+ parts,
1785
+ });
1786
+ } else {
1787
+ if (coreMessage.role === "tool" && !assistantMessage) {
1788
+ console.warn(
1789
+ "Tool message without preceding assistant message.. skipping",
1790
+ message
1791
+ );
1792
+ continue;
1793
+ }
1794
+ if (!assistantMessage) {
1795
+ assistantMessage = {
1796
+ id: message.id ?? message._id,
1797
+ createdAt: new Date(message._creationTime),
1798
+ role: "assistant",
1799
+ content: message.text ?? "",
1800
+ parts: [],
1801
+ };
1802
+ uiMessages.push(assistantMessage);
1803
+ }
1804
+ // update it to the last message's id
1805
+ assistantMessage.id = message.id ?? message._id;
1806
+ if (message.text) {
1807
+ assistantMessage.parts.push({
1808
+ type: "text",
1809
+ text: message.text,
1810
+ });
1811
+ assistantMessage.content += message.text;
1812
+ }
1813
+ if (message.reasoning) {
1814
+ assistantMessage.parts.push({
1815
+ type: "reasoning",
1816
+ reasoning: message.reasoning,
1817
+ details: message.reasoningDetails ?? [],
1818
+ });
1819
+ }
1820
+ for (const source of message.sources ?? []) {
1821
+ assistantMessage.parts.push({
1822
+ type: "source",
1823
+ source,
1824
+ });
1825
+ }
1826
+ for (const file of message.files ?? []) {
1827
+ assistantMessage.parts.push(toUIFilePart(file));
1828
+ }
1829
+ for (const contentPart of nonStringContent) {
1830
+ switch (contentPart.type) {
1831
+ case "tool-call":
1832
+ assistantMessage.parts.push({
1833
+ type: "step-start",
1834
+ });
1835
+ assistantMessage.parts.push({
1836
+ type: "tool-invocation",
1837
+ toolInvocation: {
1838
+ state: "call",
1839
+ step: assistantMessage.parts.filter(
1840
+ (part) => part.type === "tool-invocation"
1841
+ ).length,
1842
+ toolCallId: contentPart.toolCallId,
1843
+ toolName: contentPart.toolName,
1844
+ args: contentPart.args,
1845
+ },
1846
+ });
1847
+ break;
1848
+ case "tool-result": {
1849
+ const call = assistantMessage.parts.find(
1850
+ (part) =>
1851
+ part.type === "tool-invocation" &&
1852
+ part.toolInvocation.toolCallId === contentPart.toolCallId
1853
+ ) as ToolInvocationUIPart | undefined;
1854
+ const toolInvocation: ToolInvocationUIPart["toolInvocation"] = {
1855
+ state: "result",
1856
+ toolCallId: contentPart.toolCallId,
1857
+ toolName: contentPart.toolName,
1858
+ args: call?.toolInvocation.args,
1859
+ result: contentPart.result,
1860
+ step:
1861
+ call?.toolInvocation.step ??
1862
+ assistantMessage.parts.filter(
1863
+ (part) => part.type === "tool-invocation"
1864
+ ).length,
1865
+ };
1866
+ if (call) {
1867
+ (call as ToolInvocationUIPart).toolInvocation = toolInvocation;
1868
+ } else {
1869
+ console.warn(
1870
+ "Tool result without preceding tool call.. adding anyways",
1871
+ contentPart
1872
+ );
1873
+ assistantMessage.parts.push({
1874
+ type: "tool-invocation",
1875
+ toolInvocation,
1876
+ });
1877
+ }
1878
+ break;
1879
+ }
1880
+ }
1881
+ }
1882
+ if (!message.tool) {
1883
+ // Reset it so the next set of tool calls will create a new assistant message
1884
+ assistantMessage = undefined;
1885
+ }
1886
+ }
1887
+ }
1888
+ return uiMessages;
1889
+ }
@@ -5,35 +5,34 @@ import {
5
5
  actionGeneric,
6
6
  GenericDataModel,
7
7
  GenericQueryCtx,
8
- FilterApi,
9
- FunctionReference,
10
8
  ApiFromModules,
11
9
  } from "convex/server";
12
- import { vThreadDoc, type Agent } from "./index";
13
- import type { RunQueryCtx, UseApi } from "./types";
14
- import type { ToolSet } from "ai";
15
- import { v } from "convex/values";
16
- import { Mounts } from "../component/_generated/api";
17
10
  import {
11
+ vMessageDoc,
12
+ vThreadDoc,
18
13
  paginationResultValidator,
19
- vContextOptions,
20
14
  vMessage,
21
- } from "../validators";
22
- import { assert } from "convex-helpers";
15
+ vContextOptions,
16
+ vStorageOptions,
17
+ type AgentComponent,
18
+ type Agent,
19
+ } from "./index";
20
+ import type { ToolSet } from "ai";
21
+ import { v } from "convex/values";
23
22
 
24
23
  export type PlaygroundAPI = ApiFromModules<{
25
- playground: ReturnType<typeof definePlaygroundAPI<GenericDataModel>>;
24
+ playground: ReturnType<typeof definePlaygroundAPI>;
26
25
  }>["playground"];
27
26
 
28
27
  // Playground API definition
29
- export function definePlaygroundAPI<DataModel extends GenericDataModel>(
30
- component: UseApi<Mounts>,
28
+ export function definePlaygroundAPI(
29
+ component: AgentComponent,
31
30
  {
32
31
  agents,
33
32
  userNameLookup,
34
33
  }: {
35
34
  agents: Agent<ToolSet>[];
36
- userNameLookup?: (
35
+ userNameLookup?: <DataModel extends GenericDataModel>(
37
36
  ctx: GenericQueryCtx<DataModel>,
38
37
  userId: string
39
38
  ) => string | Promise<string>;
@@ -48,6 +47,22 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
48
47
  await ctx.runQuery(component.apiKeys.validate, { apiKey });
49
48
  }
50
49
 
50
+ const isApiKeyValid = queryGeneric({
51
+ args: {
52
+ apiKey: v.string(),
53
+ },
54
+ handler: async (ctx, args) => {
55
+ try {
56
+ await validateApiKey(ctx, args.apiKey);
57
+ return true;
58
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
59
+ } catch (e) {
60
+ return false;
61
+ }
62
+ },
63
+ returns: v.boolean(),
64
+ });
65
+
51
66
  // List all agents
52
67
  const listAgents = queryGeneric({
53
68
  args: {
@@ -74,7 +89,7 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
74
89
  ...users,
75
90
  page: await Promise.all(
76
91
  users.page.map(async (userId) => ({
77
- id: userId,
92
+ _id: userId,
78
93
  name: userNameLookup ? await userNameLookup(ctx, userId) : userId,
79
94
  }))
80
95
  ),
@@ -82,7 +97,7 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
82
97
  },
83
98
  returns: paginationResultValidator(
84
99
  v.object({
85
- id: v.string(),
100
+ _id: v.string(),
86
101
  name: v.string(),
87
102
  })
88
103
  ),
@@ -128,6 +143,13 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
128
143
  ),
129
144
  };
130
145
  },
146
+ returns: paginationResultValidator(
147
+ v.object({
148
+ ...vThreadDoc.fields,
149
+ latestMessage: v.optional(v.string()),
150
+ lastMessageAt: v.optional(v.number()),
151
+ })
152
+ ),
131
153
  });
132
154
 
133
155
  // List messages for a thread (query)
@@ -146,6 +168,7 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
146
168
  statuses: ["success", "failed", "pending"],
147
169
  });
148
170
  },
171
+ returns: paginationResultValidator(vMessageDoc),
149
172
  });
150
173
 
151
174
  // Create a thread (mutation)
@@ -167,6 +190,7 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
167
190
  summary: args.summary,
168
191
  });
169
192
  },
193
+ returns: v.object({ threadId: v.string() }),
170
194
  });
171
195
 
172
196
  // Send a message (action)
@@ -176,20 +200,19 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
176
200
  agentName: v.string(),
177
201
  userId: v.string(),
178
202
  threadId: v.string(),
203
+ // Args passed through to generateText
179
204
  prompt: v.optional(v.string()),
180
205
  messages: v.optional(v.array(vMessage)),
181
- // Add more args as needed
206
+ contextOptions: v.optional(vContextOptions),
207
+ storageOptions: v.optional(vStorageOptions),
182
208
  },
183
209
  handler: async (ctx, args) => {
184
- await validateApiKey(ctx, args.apiKey);
185
- const { threadId, userId, messages, prompt, agentName } = args;
210
+ const { apiKey, agentName, userId, threadId, ...rest } = args;
211
+ await validateApiKey(ctx, apiKey);
186
212
  const agent = agentMap[agentName];
187
213
  if (!agent) throw new Error(`Unknown agent: ${agentName}`);
188
214
  const { thread } = await agent.continueThread(ctx, { threadId, userId });
189
- // Prefer messages if provided, else prompt
190
- assert(messages || prompt, "Must provide either messages or prompt");
191
- assert(!messages || !prompt, "Provide messages or prompt, not both");
192
- const result = await thread.generateText({ prompt, messages });
215
+ const result = await thread.generateText(rest);
193
216
  return result;
194
217
  },
195
218
  });
@@ -220,6 +243,7 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
220
243
  });
221
244
 
222
245
  return {
246
+ isApiKeyValid,
223
247
  listUsers,
224
248
  listThreads,
225
249
  listMessages,
@@ -229,3 +253,5 @@ export function definePlaygroundAPI<DataModel extends GenericDataModel>(
229
253
  fetchPromptContext,
230
254
  };
231
255
  }
256
+
257
+ type RunQueryCtx = { runQuery: GenericQueryCtx<GenericDataModel>["runQuery"] };