@convex-dev/agent 0.1.15-alpha.0 → 0.1.15-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 (50) hide show
  1. package/README.md +13 -12
  2. package/dist/client/index.d.ts +494 -173
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +148 -170
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/listMessages.d.ts +22 -0
  7. package/dist/client/listMessages.d.ts.map +1 -0
  8. package/dist/client/listMessages.js +25 -0
  9. package/dist/client/listMessages.js.map +1 -0
  10. package/dist/client/search.d.ts +162 -0
  11. package/dist/client/search.d.ts.map +1 -0
  12. package/dist/client/search.js +113 -0
  13. package/dist/client/search.js.map +1 -0
  14. package/dist/client/streaming.d.ts +17 -3
  15. package/dist/client/streaming.d.ts.map +1 -1
  16. package/dist/client/streaming.js +32 -0
  17. package/dist/client/streaming.js.map +1 -1
  18. package/dist/client/types.d.ts +5 -1
  19. package/dist/client/types.d.ts.map +1 -1
  20. package/dist/component/_generated/api.d.ts +2 -2
  21. package/dist/component/messages.d.ts +3 -3
  22. package/dist/component/messages.d.ts.map +1 -1
  23. package/dist/component/messages.js +12 -7
  24. package/dist/component/messages.js.map +1 -1
  25. package/dist/component/vector/index.js +3 -3
  26. package/dist/component/vector/index.js.map +1 -1
  27. package/dist/react/optimisticallySendMessage.d.ts +1 -0
  28. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  29. package/dist/react/optimisticallySendMessage.js +8 -1
  30. package/dist/react/optimisticallySendMessage.js.map +1 -1
  31. package/dist/react/toUIMessages.d.ts +1 -0
  32. package/dist/react/toUIMessages.d.ts.map +1 -1
  33. package/dist/react/toUIMessages.js +2 -0
  34. package/dist/react/toUIMessages.js.map +1 -1
  35. package/dist/validators.d.ts +41 -52
  36. package/dist/validators.d.ts.map +1 -1
  37. package/dist/validators.js +1 -8
  38. package/dist/validators.js.map +1 -1
  39. package/package.json +6 -4
  40. package/src/client/index.ts +283 -274
  41. package/src/client/listMessages.ts +38 -0
  42. package/src/client/search.ts +172 -0
  43. package/src/client/streaming.ts +49 -2
  44. package/src/client/types.ts +5 -1
  45. package/src/component/_generated/api.d.ts +2 -2
  46. package/src/component/messages.ts +13 -7
  47. package/src/component/vector/index.ts +4 -4
  48. package/src/react/optimisticallySendMessage.ts +11 -1
  49. package/src/react/toUIMessages.ts +3 -0
  50. package/src/validators.ts +2 -10
@@ -0,0 +1,38 @@
1
+ import type { PaginationOptions, PaginationResult } from "convex/server";
2
+ import type { AgentComponent, RunQueryCtx } from "./types.js";
3
+ import type { MessageStatus } from "../validators.js";
4
+ import type { MessageDoc } from "../component/schema.js";
5
+
6
+ /**
7
+ * List messages from a thread.
8
+ * @param ctx A ctx object from a query, mutation, or action.
9
+ * @param component The agent component, usually `components.agent`.
10
+ * @param args.threadId The thread to list messages from.
11
+ * @param args.paginationOpts Pagination options (e.g. via usePaginatedQuery).
12
+ * @param args.excludeToolMessages Whether to exclude tool messages.
13
+ * False by default.
14
+ * @param args.statuses What statuses to include. All by default.
15
+ * @returns The MessageDoc's in a format compatible with usePaginatedQuery.
16
+ */
17
+ export async function listMessages(
18
+ ctx: RunQueryCtx,
19
+ component: AgentComponent,
20
+ args: {
21
+ threadId: string;
22
+ paginationOpts: PaginationOptions;
23
+ excludeToolMessages?: boolean;
24
+ statuses?: MessageStatus[];
25
+ }
26
+ ): Promise<PaginationResult<MessageDoc>> {
27
+ if (args.paginationOpts.numItems === 0) {
28
+ return {
29
+ page: [],
30
+ isDone: true,
31
+ continueCursor: args.paginationOpts.cursor ?? "",
32
+ };
33
+ }
34
+ return ctx.runQuery(component.messages.listMessagesByThreadId, {
35
+ order: "desc",
36
+ ...args,
37
+ });
38
+ }
@@ -0,0 +1,172 @@
1
+ import type {
2
+ AgentComponent,
3
+ ContextOptions,
4
+ RunActionCtx,
5
+ RunQueryCtx,
6
+ } from "./types.js";
7
+ import type { MessageDoc } from "../component/schema.js";
8
+ import type { CoreMessage } from "ai";
9
+ import { assert } from "convex-helpers";
10
+ import {
11
+ DEFAULT_MESSAGE_RANGE,
12
+ DEFAULT_RECENT_MESSAGES,
13
+ extractText,
14
+ } from "../shared.js";
15
+
16
+ const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
17
+
18
+ export type GetEmbedding = (text: string) => Promise<{
19
+ vector: number[];
20
+ vectorModel: string;
21
+ vectorScoreThreshold?: number;
22
+ }>;
23
+
24
+ /**
25
+ * Fetch the context messages for a thread.
26
+ * @param ctx Either a query, mutation, or action ctx.
27
+ * If it is not an action context, you can't do text or
28
+ * vector search.
29
+ * @param args The associated thread, user, message
30
+ * @returns
31
+ */
32
+ export async function fetchContextMessages(
33
+ ctx: RunQueryCtx | RunActionCtx,
34
+ component: AgentComponent,
35
+ args: {
36
+ userId: string | undefined;
37
+ threadId: string | undefined;
38
+ messages: CoreMessage[];
39
+ /**
40
+ * If provided, it will search for messages up to and including this message.
41
+ * Note: if this is far in the past, text and vector search results may be more
42
+ * limited, as it's post-filtering the results.
43
+ */
44
+ upToAndIncludingMessageId?: string;
45
+ contextOptions: ContextOptions;
46
+ getEmbedding?: GetEmbedding;
47
+ }
48
+ ): Promise<MessageDoc[]> {
49
+ assert(args.userId || args.threadId, "Specify userId or threadId");
50
+ const opts = args.contextOptions;
51
+ // Fetch the latest messages from the thread
52
+ let included: Set<string> | undefined;
53
+ const contextMessages: MessageDoc[] = [];
54
+ if (
55
+ args.threadId &&
56
+ (opts.recentMessages !== 0 || args.upToAndIncludingMessageId)
57
+ ) {
58
+ const { page } = await ctx.runQuery(
59
+ component.messages.listMessagesByThreadId,
60
+ {
61
+ threadId: args.threadId,
62
+ excludeToolMessages: opts.excludeToolMessages,
63
+ paginationOpts: {
64
+ numItems: opts.recentMessages ?? DEFAULT_RECENT_MESSAGES,
65
+ cursor: null,
66
+ },
67
+ upToAndIncludingMessageId: args.upToAndIncludingMessageId,
68
+ order: "desc",
69
+ statuses: ["success"],
70
+ }
71
+ );
72
+ included = new Set(page.map((m) => m._id));
73
+ contextMessages.push(
74
+ // Reverse since we fetched in descending order
75
+ ...page.reverse()
76
+ );
77
+ }
78
+ if (opts.searchOptions?.textSearch || opts.searchOptions?.vectorSearch) {
79
+ const targetMessage = contextMessages.find(
80
+ (m) => m._id === args.upToAndIncludingMessageId
81
+ )?.message;
82
+ const messagesToSearch = targetMessage ? [targetMessage] : args.messages;
83
+ if (!("runAction" in ctx)) {
84
+ throw new Error("searchUserMessages only works in an action");
85
+ }
86
+ const lastMessage = messagesToSearch.at(-1)!;
87
+ assert(lastMessage, "No messages to search");
88
+ const text = extractText(lastMessage);
89
+ assert(text, `No text to search in message ${JSON.stringify(lastMessage)}`);
90
+ assert(
91
+ !args.contextOptions?.searchOptions?.vectorSearch || "runAction" in ctx,
92
+ "You must do vector search from an action"
93
+ );
94
+ if (opts.searchOptions?.vectorSearch && !args.getEmbedding) {
95
+ throw new Error(
96
+ "You must provide an embedding and embeddingModel to use vector search"
97
+ );
98
+ }
99
+ const embeddingFields = opts.searchOptions?.vectorSearch
100
+ ? await args.getEmbedding?.(text)
101
+ : undefined;
102
+ const searchMessages = await ctx.runAction(
103
+ component.messages.searchMessages,
104
+ {
105
+ searchAllMessagesForUserId: opts?.searchOtherThreads
106
+ ? args.userId ??
107
+ (args.threadId &&
108
+ (
109
+ await ctx.runQuery(component.threads.getThread, {
110
+ threadId: args.threadId,
111
+ })
112
+ )?.userId)
113
+ : undefined,
114
+ threadId: args.threadId,
115
+ beforeMessageId: args.upToAndIncludingMessageId,
116
+ limit: opts.searchOptions?.limit ?? 10,
117
+ messageRange: {
118
+ ...DEFAULT_MESSAGE_RANGE,
119
+ ...opts.searchOptions?.messageRange,
120
+ },
121
+ text,
122
+ vectorScoreThreshold:
123
+ opts.searchOptions?.vectorScoreThreshold ??
124
+ DEFAULT_VECTOR_SCORE_THRESHOLD,
125
+ ...embeddingFields,
126
+ }
127
+ );
128
+ // TODO: track what messages we used for context
129
+ contextMessages.unshift(
130
+ ...searchMessages.filter((m) => !included?.has(m._id))
131
+ );
132
+ }
133
+ // Ensure we don't include tool messages without a corresponding tool call
134
+ return filterOutOrphanedToolMessages(
135
+ contextMessages.sort((a, b) =>
136
+ // Sort the raw MessageDocs by order and stepOrder
137
+ a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order
138
+ )
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Filter out tool messages that don't have both a tool call and response.
144
+ * @param docs The messages to filter.
145
+ * @returns The filtered messages.
146
+ */
147
+ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
148
+ const toolCallIds = new Set<string>();
149
+ const result: MessageDoc[] = [];
150
+ for (const doc of docs) {
151
+ if (
152
+ doc.message?.role === "assistant" &&
153
+ Array.isArray(doc.message.content)
154
+ ) {
155
+ for (const content of doc.message.content) {
156
+ if (content.type === "tool-call") {
157
+ toolCallIds.add(content.toolCallId);
158
+ }
159
+ }
160
+ result.push(doc);
161
+ } else if (doc.message?.role === "tool") {
162
+ if (doc.message.content.every((c) => toolCallIds.has(c.toolCallId))) {
163
+ result.push(doc);
164
+ } else {
165
+ console.debug("Filtering out orphaned tool message", doc);
166
+ }
167
+ } else {
168
+ result.push(doc);
169
+ }
170
+ }
171
+ return result;
172
+ }
@@ -6,11 +6,58 @@ import {
6
6
  } from "ai";
7
7
  import type {
8
8
  ProviderOptions,
9
+ StreamArgs,
9
10
  StreamDelta,
10
11
  TextStreamPart,
11
12
  } from "../validators.js";
12
- import type { AgentComponent, MessageDoc } from "./index.js";
13
- import type { RunActionCtx } from "./types.js";
13
+ import type { MessageDoc } from "../component/schema.js";
14
+ import type {
15
+ AgentComponent,
16
+ RunActionCtx,
17
+ RunQueryCtx,
18
+ SyncStreamsReturnValue,
19
+ } from "./types.js";
20
+
21
+ /**
22
+ * A function that handles fetching stream deltas, used with the React hooks
23
+ * `useThreadMessages` or `useStreamingThreadMessages`.
24
+ * @param ctx A ctx object from a query, mutation, or action.
25
+ * @param component The agent component, usually `components.agent`.
26
+ * @param args.threadId The thread to sync streams for.
27
+ * @param args.streamArgs The stream arguments with per-stream cursors.
28
+ * @returns The deltas for each stream from their existing cursor.
29
+ */
30
+ export async function syncStreams(
31
+ ctx: RunQueryCtx,
32
+ component: AgentComponent,
33
+ args: {
34
+ threadId: string;
35
+ streamArgs: StreamArgs | undefined;
36
+ // By default, only streaming messages are included.
37
+ includeStatuses?: ("streaming" | "finished" | "aborted")[];
38
+ }
39
+ ): Promise<SyncStreamsReturnValue | undefined> {
40
+ if (!args.streamArgs) return undefined;
41
+ if (args.streamArgs.kind === "list") {
42
+ return {
43
+ kind: "list",
44
+ messages: await ctx.runQuery(component.streams.list, {
45
+ threadId: args.threadId,
46
+ startOrder: args.streamArgs.startOrder,
47
+ statuses: args.includeStatuses,
48
+ }),
49
+ };
50
+ } else {
51
+ return {
52
+ kind: "deltas",
53
+ deltas: await ctx.runQuery(component.streams.listDeltas, {
54
+ threadId: args.threadId,
55
+ cursors: args.streamArgs.cursors,
56
+ }),
57
+ };
58
+ }
59
+ }
60
+
14
61
 
15
62
  export type StreamingOptions = {
16
63
  /**
@@ -77,6 +77,10 @@ export type ContextOptions = {
77
77
  * At least one of textSearch or vectorSearch must be true.
78
78
  */
79
79
  vectorSearch?: boolean;
80
+ /**
81
+ * The score threshold for vector search. Default is 0.0.
82
+ */
83
+ vectorScoreThreshold?: number;
80
84
  /**
81
85
  * What messages around the search results to include.
82
86
  * Default: { before: 2, after: 1 }
@@ -144,7 +148,7 @@ export type UsageHandler = (
144
148
  ) => void | Promise<void>;
145
149
 
146
150
  export type RawRequestResponseHandler = (
147
- ctx: RunActionCtx,
151
+ ctx: ActionCtx,
148
152
  args: {
149
153
  userId: string | undefined;
150
154
  threadId: string | undefined;
@@ -900,13 +900,13 @@ export type Mounts = {
900
900
  "public",
901
901
  {
902
902
  beforeMessageId?: string;
903
+ embedding?: Array<number>;
904
+ embeddingModel?: string;
903
905
  limit: number;
904
906
  messageRange?: { after: number; before: number };
905
907
  searchAllMessagesForUserId?: string;
906
908
  text?: string;
907
909
  threadId?: string;
908
- vector?: Array<number>;
909
- vectorModel?: string;
910
910
  vectorScoreThreshold?: number;
911
911
  },
912
912
  Array<{
@@ -13,7 +13,6 @@ import {
13
13
  vMessageStatus,
14
14
  vMessageWithMetadataInternal,
15
15
  vPaginationResult,
16
- vSearchOptions,
17
16
  } from "../validators.js";
18
17
  import { api, internal } from "./_generated/api.js";
19
18
  import type { Doc, Id } from "./_generated/dataModel.js";
@@ -467,7 +466,14 @@ export const searchMessages = action({
467
466
  threadId: v.optional(v.id("threads")),
468
467
  searchAllMessagesForUserId: v.optional(v.string()),
469
468
  beforeMessageId: v.optional(v.id("messages")),
470
- ...vSearchOptions.fields,
469
+ embedding: v.optional(v.array(v.number())),
470
+ embeddingModel: v.optional(v.string()),
471
+ text: v.optional(v.string()),
472
+ limit: v.number(),
473
+ vectorScoreThreshold: v.optional(v.number()),
474
+ messageRange: v.optional(
475
+ v.object({ before: v.number(), after: v.number() })
476
+ ),
471
477
  },
472
478
  returns: v.array(vMessageDoc),
473
479
  handler: async (ctx, args): Promise<MessageDoc[]> => {
@@ -486,15 +492,15 @@ export const searchMessages = action({
486
492
  beforeMessageId: args.beforeMessageId,
487
493
  });
488
494
  }
489
- if (args.vector) {
490
- const dimension = args.vector.length as VectorDimension;
495
+ if (args.embedding) {
496
+ const dimension = args.embedding.length as VectorDimension;
491
497
  if (!VectorDimensions.includes(dimension)) {
492
- throw new Error(`Unsupported vector dimension: ${dimension}`);
498
+ throw new Error(`Unsupported embedding dimension: ${dimension}`);
493
499
  }
494
500
  const vectors = (
495
- await searchVectors(ctx, args.vector, {
501
+ await searchVectors(ctx, args.embedding, {
496
502
  dimension,
497
- model: args.vectorModel ?? "unknown",
503
+ model: args.embeddingModel ?? "unknown",
498
504
  table: "messages",
499
505
  searchAllMessagesForUserId: args.searchAllMessagesForUserId,
500
506
  threadId: args.threadId,
@@ -182,10 +182,10 @@ export const updateBatch = mutation({
182
182
  returns: v.null(),
183
183
  handler: async (ctx, args) => {
184
184
  await Promise.all(
185
- args.vectors.map((v) =>
186
- ctx.db.patch(v.id, {
187
- model: v.model,
188
- vector: v.vector,
185
+ args.vectors.map((embedding) =>
186
+ ctx.db.patch(embedding.id, {
187
+ model: embedding.model,
188
+ vector: embedding.vector,
189
189
  })
190
190
  )
191
191
  );
@@ -28,7 +28,7 @@ export function optimisticallySendMessage(
28
28
  argsToMatch: { threadId: args.threadId, streamArgs: undefined },
29
29
  item: {
30
30
  _creationTime: Date.now(),
31
- _id: crypto.randomUUID(),
31
+ _id: randomUUID(),
32
32
  order,
33
33
  stepOrder,
34
34
  status: "pending",
@@ -44,3 +44,13 @@ export function optimisticallySendMessage(
44
44
  });
45
45
  };
46
46
  }
47
+
48
+ export function randomUUID() {
49
+ if (typeof crypto !== "undefined") {
50
+ return crypto.randomUUID();
51
+ }
52
+ return (
53
+ Math.random().toString(36).substring(2, 15) +
54
+ Math.random().toString(36).substring(2, 15)
55
+ );
56
+ }
@@ -9,6 +9,7 @@ export type UIMessage = AIUIMessage & {
9
9
  order: number;
10
10
  stepOrder: number;
11
11
  status: "streaming" | MessageStatus;
12
+ agentName?: string;
12
13
  };
13
14
 
14
15
  export function toUIMessages(
@@ -36,6 +37,7 @@ export function toUIMessages(
36
37
  ...common,
37
38
  role: "system",
38
39
  content: text,
40
+ agentName: message.agentName,
39
41
  parts: [{ type: "text", text }],
40
42
  });
41
43
  } else if (coreMessage.role === "user") {
@@ -69,6 +71,7 @@ export function toUIMessages(
69
71
  assistantMessage = {
70
72
  ...common,
71
73
  role: "assistant",
74
+ agentName: message.agentName,
72
75
  content: "",
73
76
  parts: [],
74
77
  };
package/src/validators.ts CHANGED
@@ -277,7 +277,7 @@ export const vMessageEmbeddings = v.object({
277
277
  dimension: vVectorDimension,
278
278
  vectors: v.array(v.union(v.array(v.number()), v.null())),
279
279
  });
280
-
280
+ export type MessageEmbeddings = Infer<typeof vMessageEmbeddings>;
281
281
 
282
282
  export const vObjectResult = v.object({
283
283
  request: vRequest,
@@ -290,20 +290,12 @@ export const vObjectResult = v.object({
290
290
  providerMetadata,
291
291
  });
292
292
  export type ObjectResult = Infer<typeof vObjectResult>;
293
- export const vSearchOptions = v.object({
294
- vector: v.optional(v.array(v.number())),
295
- vectorModel: v.optional(v.string()),
296
- text: v.optional(v.string()),
297
- limit: v.number(),
298
- vectorScoreThreshold: v.optional(v.number()),
299
- messageRange: v.optional(v.object({ before: v.number(), after: v.number() })),
300
- });
301
- export type SearchOptions = Infer<typeof vSearchOptions>;
302
293
 
303
294
  export const vContextOptionsSearchOptions = v.object({
304
295
  limit: v.number(),
305
296
  textSearch: v.optional(v.boolean()),
306
297
  vectorSearch: v.optional(v.boolean()),
298
+ vectorScoreThreshold: v.optional(v.number()),
307
299
  messageRange: v.optional(v.object({ before: v.number(), after: v.number() })),
308
300
  });
309
301