@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.
Files changed (235) hide show
  1. package/MIGRATION.md +153 -0
  2. package/README.md +32 -27
  3. package/dist/UIMessages.d.ts +46 -0
  4. package/dist/UIMessages.d.ts.map +1 -0
  5. package/dist/UIMessages.js +546 -0
  6. package/dist/UIMessages.js.map +1 -0
  7. package/dist/client/createTool.d.ts +126 -27
  8. package/dist/client/createTool.d.ts.map +1 -1
  9. package/dist/client/createTool.js +67 -12
  10. package/dist/client/createTool.js.map +1 -1
  11. package/dist/client/defaultComponent.d.ts +11 -0
  12. package/dist/client/defaultComponent.d.ts.map +1 -0
  13. package/dist/client/defaultComponent.js +7 -0
  14. package/dist/client/defaultComponent.js.map +1 -0
  15. package/dist/client/definePlaygroundAPI.d.ts +1335 -204
  16. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  17. package/dist/client/definePlaygroundAPI.js +52 -28
  18. package/dist/client/definePlaygroundAPI.js.map +1 -1
  19. package/dist/client/files.d.ts +20 -7
  20. package/dist/client/files.d.ts.map +1 -1
  21. package/dist/client/files.js +68 -11
  22. package/dist/client/files.js.map +1 -1
  23. package/dist/client/index.d.ts +1116 -978
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +332 -747
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/messages.d.ts +461 -0
  28. package/dist/client/messages.d.ts.map +1 -0
  29. package/dist/client/messages.js +106 -0
  30. package/dist/client/messages.js.map +1 -0
  31. package/dist/client/mockModel.d.ts +42 -0
  32. package/dist/client/mockModel.d.ts.map +1 -0
  33. package/dist/client/mockModel.js +182 -0
  34. package/dist/client/mockModel.js.map +1 -0
  35. package/dist/client/saveInputMessages.d.ts +20 -0
  36. package/dist/client/saveInputMessages.d.ts.map +1 -0
  37. package/dist/client/saveInputMessages.js +58 -0
  38. package/dist/client/saveInputMessages.js.map +1 -0
  39. package/dist/client/search.d.ts +350 -39
  40. package/dist/client/search.d.ts.map +1 -1
  41. package/dist/client/search.js +350 -39
  42. package/dist/client/search.js.map +1 -1
  43. package/dist/client/start.d.ts +84 -0
  44. package/dist/client/start.d.ts.map +1 -0
  45. package/dist/client/start.js +185 -0
  46. package/dist/client/start.js.map +1 -0
  47. package/dist/client/streamText.d.ts +46 -0
  48. package/dist/client/streamText.d.ts.map +1 -0
  49. package/dist/client/streamText.js +117 -0
  50. package/dist/client/streamText.js.map +1 -0
  51. package/dist/client/streaming.d.ts +3716 -32
  52. package/dist/client/streaming.d.ts.map +1 -1
  53. package/dist/client/streaming.js +161 -59
  54. package/dist/client/streaming.js.map +1 -1
  55. package/dist/client/threads.d.ts +46 -0
  56. package/dist/client/threads.d.ts.map +1 -0
  57. package/dist/client/threads.js +49 -0
  58. package/dist/client/threads.js.map +1 -0
  59. package/dist/client/types.d.ts +266 -128
  60. package/dist/client/types.d.ts.map +1 -1
  61. package/dist/client/utils.d.ts +4 -0
  62. package/dist/client/utils.d.ts.map +1 -0
  63. package/dist/client/utils.js +21 -0
  64. package/dist/client/utils.js.map +1 -0
  65. package/dist/component/_generated/api.d.ts +24 -2178
  66. package/dist/component/_generated/api.d.ts.map +1 -1
  67. package/dist/component/_generated/api.js +10 -1
  68. package/dist/component/_generated/api.js.map +1 -1
  69. package/dist/component/_generated/component.d.ts +3120 -0
  70. package/dist/component/_generated/component.d.ts.map +1 -0
  71. package/dist/component/_generated/component.js +11 -0
  72. package/dist/component/_generated/component.js.map +1 -0
  73. package/dist/component/_generated/dataModel.d.ts +4 -18
  74. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  75. package/dist/component/_generated/dataModel.js +11 -0
  76. package/dist/component/_generated/dataModel.js.map +1 -0
  77. package/dist/component/_generated/server.d.ts +10 -38
  78. package/dist/component/_generated/server.d.ts.map +1 -1
  79. package/dist/component/_generated/server.js +9 -5
  80. package/dist/component/_generated/server.js.map +1 -1
  81. package/dist/component/files.d.ts +16 -10
  82. package/dist/component/files.d.ts.map +1 -1
  83. package/dist/component/files.js +10 -2
  84. package/dist/component/files.js.map +1 -1
  85. package/dist/component/messages.d.ts +2578 -366
  86. package/dist/component/messages.d.ts.map +1 -1
  87. package/dist/component/messages.js +397 -154
  88. package/dist/component/messages.js.map +1 -1
  89. package/dist/component/schema.d.ts +5697 -3584
  90. package/dist/component/schema.d.ts.map +1 -1
  91. package/dist/component/schema.js +18 -41
  92. package/dist/component/schema.js.map +1 -1
  93. package/dist/component/streams.d.ts +39 -339
  94. package/dist/component/streams.d.ts.map +1 -1
  95. package/dist/component/streams.js +114 -73
  96. package/dist/component/streams.js.map +1 -1
  97. package/dist/component/threads.d.ts +13 -13
  98. package/dist/component/users.d.ts +7 -7
  99. package/dist/component/vector/index.d.ts +1 -1
  100. package/dist/component/vector/index.d.ts.map +1 -1
  101. package/dist/component/vector/index.js +1 -3
  102. package/dist/component/vector/index.js.map +1 -1
  103. package/dist/deltas.d.ts +43 -0
  104. package/dist/deltas.d.ts.map +1 -0
  105. package/dist/deltas.js +446 -0
  106. package/dist/deltas.js.map +1 -0
  107. package/dist/mapping.d.ts +38 -20
  108. package/dist/mapping.d.ts.map +1 -1
  109. package/dist/mapping.js +365 -97
  110. package/dist/mapping.js.map +1 -1
  111. package/dist/react/SmoothText.d.ts +5 -0
  112. package/dist/react/SmoothText.d.ts.map +1 -0
  113. package/dist/react/SmoothText.js +6 -0
  114. package/dist/react/SmoothText.js.map +1 -0
  115. package/dist/react/index.d.ts +5 -77
  116. package/dist/react/index.d.ts.map +1 -1
  117. package/dist/react/index.js +6 -160
  118. package/dist/react/index.js.map +1 -1
  119. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  120. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  121. package/dist/react/optimisticallySendMessage.js +35 -9
  122. package/dist/react/optimisticallySendMessage.js.map +1 -1
  123. package/dist/react/types.d.ts +4 -18
  124. package/dist/react/types.d.ts.map +1 -1
  125. package/dist/react/useDeltaStreams.d.ts +10 -0
  126. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  127. package/dist/react/useDeltaStreams.js +106 -0
  128. package/dist/react/useDeltaStreams.js.map +1 -0
  129. package/dist/react/useSmoothText.d.ts +13 -12
  130. package/dist/react/useSmoothText.d.ts.map +1 -1
  131. package/dist/react/useSmoothText.js +32 -15
  132. package/dist/react/useSmoothText.js.map +1 -1
  133. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  134. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  135. package/dist/react/useStreamingUIMessages.js +92 -0
  136. package/dist/react/useStreamingUIMessages.js.map +1 -0
  137. package/dist/react/useThreadMessages.d.ts +104 -0
  138. package/dist/react/useThreadMessages.d.ts.map +1 -0
  139. package/dist/react/useThreadMessages.js +148 -0
  140. package/dist/react/useThreadMessages.js.map +1 -0
  141. package/dist/react/useUIMessages.d.ts +96 -0
  142. package/dist/react/useUIMessages.d.ts.map +1 -0
  143. package/dist/react/useUIMessages.js +108 -0
  144. package/dist/react/useUIMessages.js.map +1 -0
  145. package/dist/shared.d.ts +20 -4
  146. package/dist/shared.d.ts.map +1 -1
  147. package/dist/shared.js +45 -8
  148. package/dist/shared.js.map +1 -1
  149. package/dist/validators.d.ts +22981 -5666
  150. package/dist/validators.d.ts.map +1 -1
  151. package/dist/validators.js +245 -137
  152. package/dist/validators.js.map +1 -1
  153. package/package.json +101 -51
  154. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  155. package/src/UIMessages.test.ts +273 -0
  156. package/src/UIMessages.ts +739 -0
  157. package/src/client/approval.test.ts +350 -0
  158. package/src/client/createTool.ts +291 -76
  159. package/src/client/defaultComponent.ts +17 -0
  160. package/src/client/definePlaygroundAPI.ts +67 -31
  161. package/src/client/files.ts +100 -20
  162. package/src/client/index.test.ts +40 -85
  163. package/src/client/index.ts +638 -1289
  164. package/src/client/messages.ts +237 -0
  165. package/src/client/mockModel.ts +252 -0
  166. package/src/client/saveInputMessages.test.ts +583 -0
  167. package/src/client/saveInputMessages.ts +101 -0
  168. package/src/client/search.test.ts +1207 -0
  169. package/src/client/search.ts +581 -70
  170. package/src/client/start.ts +327 -0
  171. package/src/client/streamText.ts +187 -0
  172. package/src/client/streaming.test.ts +186 -0
  173. package/src/client/streaming.ts +241 -97
  174. package/src/client/threads.ts +83 -0
  175. package/src/client/types.ts +370 -219
  176. package/src/client/utils.ts +27 -0
  177. package/src/component/_generated/api.ts +64 -0
  178. package/src/component/_generated/component.ts +4902 -0
  179. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  180. package/src/component/files.ts +11 -2
  181. package/src/component/messages.test.ts +195 -51
  182. package/src/component/messages.ts +500 -201
  183. package/src/component/schema.ts +20 -46
  184. package/src/component/setup.test.ts +7 -0
  185. package/src/component/streams.ts +184 -83
  186. package/src/component/users.test.ts +0 -1
  187. package/src/component/vector/index.ts +1 -3
  188. package/src/deltas.test.ts +626 -0
  189. package/src/deltas.ts +569 -0
  190. package/src/fromUIMessages.test.ts +497 -0
  191. package/src/mapping.test.ts +180 -6
  192. package/src/mapping.ts +479 -162
  193. package/src/react/SmoothText.tsx +9 -0
  194. package/src/react/index.ts +10 -230
  195. package/src/react/optimisticallySendMessage.ts +55 -12
  196. package/src/react/types.ts +6 -39
  197. package/src/react/useDeltaStreams.ts +160 -0
  198. package/src/react/useSmoothText.ts +56 -36
  199. package/src/react/useStreamingUIMessages.ts +143 -0
  200. package/src/react/useThreadMessages.ts +262 -0
  201. package/src/react/useUIMessages.test.ts +255 -0
  202. package/src/react/useUIMessages.ts +195 -0
  203. package/src/shared.ts +88 -12
  204. package/src/test.ts +18 -0
  205. package/src/toUIMessages.test.ts +1269 -0
  206. package/src/validators.test.ts +18 -19
  207. package/src/validators.ts +325 -185
  208. package/dist/client/_generated/_ignore.d.ts +0 -1
  209. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  210. package/dist/client/_generated/_ignore.js +0 -3
  211. package/dist/client/_generated/_ignore.js.map +0 -1
  212. package/dist/client/listMessages.d.ts +0 -22
  213. package/dist/client/listMessages.d.ts.map +0 -1
  214. package/dist/client/listMessages.js +0 -25
  215. package/dist/client/listMessages.js.map +0 -1
  216. package/dist/package.json +0 -3
  217. package/dist/react/deltas.d.ts +0 -26
  218. package/dist/react/deltas.d.ts.map +0 -1
  219. package/dist/react/deltas.js +0 -384
  220. package/dist/react/deltas.js.map +0 -1
  221. package/dist/react/toUIMessages.d.ts +0 -15
  222. package/dist/react/toUIMessages.d.ts.map +0 -1
  223. package/dist/react/toUIMessages.js +0 -211
  224. package/dist/react/toUIMessages.js.map +0 -1
  225. package/src/client/listMessages.ts +0 -38
  226. package/src/component/_generated/api.d.ts +0 -2202
  227. package/src/component/_generated/api.js +0 -23
  228. package/src/component/_generated/server.js +0 -90
  229. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  230. package/src/react/deltas.test.ts +0 -315
  231. package/src/react/deltas.ts +0 -478
  232. package/src/react/toUIMessages.test.ts +0 -420
  233. package/src/react/toUIMessages.ts +0 -253
  234. package/src/vitest.config.ts +0 -7
  235. /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
@@ -1,25 +1,59 @@
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 { ModelMessage } from "ai";
1
+ import {
2
+ embedMany as embedMany_,
3
+ type EmbeddingModel,
4
+ type ModelMessage,
5
+ } from "ai";
9
6
  import { assert } from "convex-helpers";
7
+ import type { MessageDoc } from "../validators.js";
8
+ import {
9
+ validateVectorDimension,
10
+ type VectorDimension,
11
+ } from "../component/vector/tables.js";
10
12
  import {
11
13
  DEFAULT_MESSAGE_RANGE,
12
14
  DEFAULT_RECENT_MESSAGES,
13
15
  extractText,
16
+ getModelName,
17
+ getProviderName,
18
+ isTool,
19
+ sorted,
14
20
  } from "../shared.js";
15
21
  import type { Message } from "../validators.js";
22
+ import type {
23
+ AgentComponent,
24
+ Config,
25
+ ContextOptions,
26
+ Options,
27
+ QueryCtx,
28
+ MutationCtx,
29
+ ActionCtx,
30
+ } from "./types.js";
31
+ import { inlineMessagesFiles } from "./files.js";
32
+ import {
33
+ docsToModelMessages,
34
+ mergeApprovalResponseMessages,
35
+ toModelMessage,
36
+ } from "../mapping.js";
16
37
 
17
38
  const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
39
+ // 10k characters should be more than enough for most cases, and stays under
40
+ // the 8k token limit for some models.
41
+ const MAX_EMBEDDING_TEXT_LENGTH = 10_000;
18
42
 
19
- export type GetEmbedding = (text: string) => Promise<{
20
- embedding: number[];
21
- embeddingModel: string;
22
- }>;
43
+ export type GetEmbedding = (text: string) => Promise<
44
+ | {
45
+ embedding: number[];
46
+ /** @deprecated Use embeddingModel instead. */
47
+ textEmbeddingModel: string | EmbeddingModel;
48
+ embeddingModel?: string | EmbeddingModel;
49
+ }
50
+ | {
51
+ embedding: number[];
52
+ /** @deprecated Use embeddingModel instead. */
53
+ textEmbeddingModel?: string | EmbeddingModel;
54
+ embeddingModel: string | EmbeddingModel;
55
+ }
56
+ >;
23
57
 
24
58
  /**
25
59
  * Fetch the context messages for a thread.
@@ -30,31 +64,78 @@ export type GetEmbedding = (text: string) => Promise<{
30
64
  * @returns
31
65
  */
32
66
  export async function fetchContextMessages(
33
- ctx: RunQueryCtx | RunActionCtx,
67
+ ctx: QueryCtx | MutationCtx | ActionCtx,
34
68
  component: AgentComponent,
35
69
  args: {
36
70
  userId: string | undefined;
37
71
  threadId: string | undefined;
38
- messages: (ModelMessage | Message)[];
39
72
  /**
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.
73
+ * If targetMessageId is not provided, this text will be used
74
+ * for text and vector search
75
+ */
76
+ searchText?: string;
77
+ /**
78
+ * If provided, it will use this message for text/vector search (if enabled)
79
+ * and will only fetch messages up to (and including) this message's "order"
80
+ */
81
+ targetMessageId?: string;
82
+ /**
83
+ * @deprecated use searchText and targetMessageId instead
84
+ */
85
+ messages?: (ModelMessage | Message)[];
86
+ /**
87
+ * @deprecated use targetMessageId instead
43
88
  */
44
89
  upToAndIncludingMessageId?: string;
45
90
  contextOptions: ContextOptions;
46
91
  getEmbedding?: GetEmbedding;
47
92
  },
48
93
  ): Promise<MessageDoc[]> {
94
+ const { recentMessages, searchMessages } = await fetchRecentAndSearchMessages(
95
+ ctx,
96
+ component,
97
+ args,
98
+ );
99
+ return [...searchMessages, ...recentMessages];
100
+ }
101
+
102
+ export async function fetchRecentAndSearchMessages(
103
+ ctx: QueryCtx | MutationCtx | ActionCtx,
104
+ component: AgentComponent,
105
+ args: {
106
+ userId: string | undefined;
107
+ threadId: string | undefined;
108
+ /**
109
+ * If targetMessageId is not provided, this text will be used
110
+ * for text and vector search
111
+ */
112
+ searchText?: string;
113
+ /**
114
+ * If provided, it will use this message for text/vector search (if enabled)
115
+ * and will only fetch messages up to (and including) this message's "order"
116
+ */
117
+ targetMessageId?: string;
118
+ /**
119
+ * @deprecated use searchText and targetMessageId instead
120
+ */
121
+ messages?: (ModelMessage | Message)[];
122
+ /**
123
+ * @deprecated use targetMessageId instead
124
+ */
125
+ upToAndIncludingMessageId?: string;
126
+ contextOptions: ContextOptions;
127
+ getEmbedding?: GetEmbedding;
128
+ },
129
+ ): Promise<{ recentMessages: MessageDoc[]; searchMessages: MessageDoc[] }> {
49
130
  assert(args.userId || args.threadId, "Specify userId or threadId");
50
131
  const opts = args.contextOptions;
51
132
  // Fetch the latest messages from the thread
52
133
  let included: Set<string> | undefined;
53
- const contextMessages: MessageDoc[] = [];
54
- if (
55
- args.threadId &&
56
- (opts.recentMessages !== 0 || args.upToAndIncludingMessageId)
57
- ) {
134
+ let recentMessages: MessageDoc[] = [];
135
+ let searchMessages: MessageDoc[] = [];
136
+ const targetMessageId =
137
+ args.targetMessageId ?? args.upToAndIncludingMessageId;
138
+ if (args.threadId && opts.recentMessages !== 0) {
58
139
  const { page } = await ctx.runQuery(
59
140
  component.messages.listMessagesByThreadId,
60
141
  {
@@ -64,106 +145,187 @@ export async function fetchContextMessages(
64
145
  numItems: opts.recentMessages ?? DEFAULT_RECENT_MESSAGES,
65
146
  cursor: null,
66
147
  },
67
- upToAndIncludingMessageId: args.upToAndIncludingMessageId,
148
+ upToAndIncludingMessageId: targetMessageId,
68
149
  order: "desc",
69
150
  statuses: ["success"],
70
151
  },
71
152
  );
72
153
  included = new Set(page.map((m) => m._id));
73
- contextMessages.push(
74
- // Reverse since we fetched in descending order
75
- ...page.reverse(),
76
- );
154
+ recentMessages = filterOutOrphanedToolMessages(sorted(page));
77
155
  }
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;
156
+ if (
157
+ (opts.searchOptions?.textSearch || opts.searchOptions?.vectorSearch) &&
158
+ opts.searchOptions?.limit
159
+ ) {
83
160
  if (!("runAction" in ctx)) {
84
161
  throw new Error("searchUserMessages only works in an action");
85
162
  }
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
- );
163
+ let text = args.searchText;
164
+ let embedding: number[] | undefined;
165
+ let embeddingModel: string | undefined;
166
+ if (!text) {
167
+ if (targetMessageId) {
168
+ const targetMessage = recentMessages.find(
169
+ (m) => m._id === targetMessageId,
170
+ );
171
+ if (targetMessage) {
172
+ text = targetMessage.text;
173
+ } else {
174
+ const targetSearchFields = await ctx.runQuery(
175
+ component.messages.getMessageSearchFields,
176
+ {
177
+ messageId: targetMessageId,
178
+ },
179
+ );
180
+ text = targetSearchFields.text;
181
+ embedding = targetSearchFields.embedding;
182
+ embeddingModel = targetSearchFields.embeddingModel;
183
+ }
184
+ assert(text, "Target message has no text for searching");
185
+ } else if (args.messages?.length) {
186
+ text = extractText(args.messages.at(-1)!);
187
+ assert(text, "Final context message has no text to search");
188
+ }
189
+ assert(text, "No text to search");
190
+ }
191
+ if (opts.searchOptions?.vectorSearch) {
192
+ if (!embedding && args.getEmbedding) {
193
+ const embeddingFields = await args.getEmbedding(text);
194
+ embedding = embeddingFields.embedding;
195
+ const effectiveModel =
196
+ embeddingFields.embeddingModel ?? embeddingFields.textEmbeddingModel;
197
+ embeddingModel = effectiveModel
198
+ ? getModelName(effectiveModel)
199
+ : undefined;
200
+ // TODO: if the text matches the target message, save the embedding
201
+ // for the target message and return the embeddingId on the message.
202
+ }
98
203
  }
99
- const embeddingFields = opts.searchOptions?.vectorSearch
100
- ? await args.getEmbedding?.(text)
101
- : undefined;
102
- const searchMessages = await ctx.runAction(
204
+ const searchResults = await ctx.runAction(
103
205
  component.messages.searchMessages,
104
206
  {
105
207
  searchAllMessagesForUserId: opts?.searchOtherThreads
106
- ? args.userId ??
208
+ ? (args.userId ??
107
209
  (args.threadId &&
108
210
  (
109
211
  await ctx.runQuery(component.threads.getThread, {
110
212
  threadId: args.threadId,
111
213
  })
112
- )?.userId)
214
+ )?.userId))
113
215
  : undefined,
114
216
  threadId: args.threadId,
115
- beforeMessageId: args.upToAndIncludingMessageId,
217
+ targetMessageId,
116
218
  limit: opts.searchOptions?.limit ?? 10,
117
219
  messageRange: {
118
220
  ...DEFAULT_MESSAGE_RANGE,
119
221
  ...opts.searchOptions?.messageRange,
120
222
  },
121
223
  text,
224
+ textSearch: opts.searchOptions?.textSearch,
225
+ vectorSearch: opts.searchOptions?.vectorSearch,
122
226
  vectorScoreThreshold:
123
227
  opts.searchOptions?.vectorScoreThreshold ??
124
228
  DEFAULT_VECTOR_SCORE_THRESHOLD,
125
- embedding: embeddingFields?.embedding,
126
- embeddingModel: embeddingFields?.embeddingModel,
229
+ embedding,
230
+ embeddingModel,
127
231
  },
128
232
  );
129
233
  // TODO: track what messages we used for context
130
- contextMessages.unshift(
131
- ...searchMessages.filter((m) => !included?.has(m._id)),
234
+ searchMessages = filterOutOrphanedToolMessages(
235
+ sorted(searchResults.filter((m) => !included?.has(m._id))),
132
236
  );
133
237
  }
134
238
  // Ensure we don't include tool messages without a corresponding tool call
135
- return filterOutOrphanedToolMessages(
136
- contextMessages.sort((a, b) =>
137
- // Sort the raw MessageDocs by order and stepOrder
138
- a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order,
139
- ),
140
- );
239
+ return { recentMessages, searchMessages };
141
240
  }
142
241
 
143
242
  /**
144
243
  * Filter out tool messages that don't have both a tool call and response.
244
+ * For the approval workflow, tool calls with approval responses (but no tool-results yet)
245
+ * should also be kept.
145
246
  * @param docs The messages to filter.
146
247
  * @returns The filtered messages.
147
248
  */
148
249
  export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
149
250
  const toolCallIds = new Set<string>();
251
+ const toolResultIds = new Set<string>();
252
+ // Track approval workflow: toolCallId → approvalId
253
+ const approvalRequestsByToolCallId = new Map<string, string>();
254
+ // Track which approvalIds have responses
255
+ const approvalResponseIds = new Set<string>();
256
+
150
257
  const result: MessageDoc[] = [];
151
258
  for (const doc of docs) {
152
- if (
153
- doc.message?.role === "assistant" &&
154
- Array.isArray(doc.message.content)
155
- ) {
259
+ if (doc.message && Array.isArray(doc.message.content)) {
156
260
  for (const content of doc.message.content) {
157
261
  if (content.type === "tool-call") {
158
262
  toolCallIds.add(content.toolCallId);
263
+ } else if (content.type === "tool-result") {
264
+ toolResultIds.add(content.toolCallId);
265
+ } else if (content.type === "tool-approval-request") {
266
+ const approvalRequest = content as {
267
+ type: "tool-approval-request";
268
+ toolCallId: string;
269
+ approvalId: string;
270
+ };
271
+ approvalRequestsByToolCallId.set(
272
+ approvalRequest.toolCallId,
273
+ approvalRequest.approvalId,
274
+ );
275
+ } else if (content.type === "tool-approval-response") {
276
+ const approvalResponse = content as {
277
+ type: "tool-approval-response";
278
+ approvalId: string;
279
+ };
280
+ approvalResponseIds.add(approvalResponse.approvalId);
159
281
  }
160
282
  }
161
- result.push(doc);
283
+ }
284
+ }
285
+
286
+ // Helper: check if tool call has a corresponding approval response
287
+ const hasApprovalResponse = (toolCallId: string) => {
288
+ const approvalId = approvalRequestsByToolCallId.get(toolCallId);
289
+ return approvalId !== undefined && approvalResponseIds.has(approvalId);
290
+ };
291
+
292
+ for (const doc of docs) {
293
+ if (
294
+ doc.message?.role === "assistant" &&
295
+ Array.isArray(doc.message.content)
296
+ ) {
297
+ const content = doc.message.content.filter(
298
+ (p) =>
299
+ p.type !== "tool-call" ||
300
+ toolResultIds.has(p.toolCallId) ||
301
+ hasApprovalResponse(p.toolCallId),
302
+ );
303
+ if (content.length) {
304
+ result.push({
305
+ ...doc,
306
+ message: {
307
+ ...doc.message,
308
+ content,
309
+ },
310
+ });
311
+ }
162
312
  } else if (doc.message?.role === "tool") {
163
- if (doc.message.content.every((c) => toolCallIds.has(c.toolCallId))) {
164
- result.push(doc);
165
- } else {
166
- console.debug("Filtering out orphaned tool message", doc);
313
+ const content = doc.message.content.filter((c) => {
314
+ // tool-result parts have toolCallId
315
+ if (c.type === "tool-result") {
316
+ return toolCallIds.has(c.toolCallId);
317
+ }
318
+ // tool-approval-response parts don't have toolCallId, so include them
319
+ return true;
320
+ });
321
+ if (content.length) {
322
+ result.push({
323
+ ...doc,
324
+ message: {
325
+ ...doc.message,
326
+ content,
327
+ },
328
+ });
167
329
  }
168
330
  } else {
169
331
  result.push(doc);
@@ -171,3 +333,352 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
171
333
  }
172
334
  return result;
173
335
  }
336
+
337
+ /**
338
+ * Embed a list of messages, including calling any usage handler.
339
+ * This will not save the embeddings to the database.
340
+ */
341
+ export async function embedMessages(
342
+ ctx: ActionCtx,
343
+ {
344
+ userId,
345
+ threadId,
346
+ ...options
347
+ }: {
348
+ userId: string | undefined;
349
+ threadId: string | undefined;
350
+ agentName?: string;
351
+ } & Pick<
352
+ Config,
353
+ "usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings"
354
+ >,
355
+ messages: (ModelMessage | Message)[],
356
+ ): Promise<
357
+ | {
358
+ vectors: (number[] | null)[];
359
+ dimension: VectorDimension;
360
+ model: string;
361
+ }
362
+ | undefined
363
+ > {
364
+ const textEmbeddingModel =
365
+ options.embeddingModel ?? options.textEmbeddingModel;
366
+ if (!textEmbeddingModel) {
367
+ return undefined;
368
+ }
369
+ let embeddings:
370
+ | {
371
+ vectors: (number[] | null)[];
372
+ dimension: VectorDimension;
373
+ model: string;
374
+ }
375
+ | undefined;
376
+ const messageTexts = messages.map((m) => !isTool(m) && extractText(m));
377
+ // Find the indexes of the messages that have text.
378
+ const textIndexes = messageTexts
379
+ .map((t, i) => (t ? i : undefined))
380
+ .filter((i) => i !== undefined);
381
+ if (textIndexes.length === 0) {
382
+ return undefined;
383
+ }
384
+ const values = messageTexts
385
+ .map((t) => t && t.trim().slice(0, MAX_EMBEDDING_TEXT_LENGTH))
386
+ .filter((t): t is string => !!t);
387
+ // Then embed those messages.
388
+ const textEmbeddings = await embedMany(ctx, {
389
+ ...options,
390
+ userId,
391
+ threadId,
392
+ values,
393
+ });
394
+ // Then assemble the embeddings into a single array with nulls for the messages without text.
395
+ const embeddingsOrNull = Array(messages.length).fill(null);
396
+ textIndexes.forEach((i, j) => {
397
+ embeddingsOrNull[i] = textEmbeddings.embeddings[j];
398
+ });
399
+ if (textEmbeddings.embeddings.length > 0) {
400
+ const dimension = textEmbeddings.embeddings[0].length;
401
+ validateVectorDimension(dimension);
402
+ const model = getModelName(textEmbeddingModel);
403
+ embeddings = { vectors: embeddingsOrNull, dimension, model };
404
+ }
405
+ return embeddings;
406
+ }
407
+
408
+ /**
409
+ * Embeds many strings, calling any usage handler.
410
+ * @param ctx The ctx parameter to an action.
411
+ * @param args Arguments to AI SDK's embedMany, and context for the embedding,
412
+ * passed to the usage handler.
413
+ * @returns The embeddings for the strings, matching the order of the values.
414
+ */
415
+ export async function embedMany(
416
+ ctx: ActionCtx,
417
+ args: {
418
+ userId: string | undefined;
419
+ threadId: string | undefined;
420
+ values: string[];
421
+ abortSignal?: AbortSignal;
422
+ headers?: Record<string, string>;
423
+ agentName?: string;
424
+ } & Pick<
425
+ Config,
426
+ "usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings"
427
+ >,
428
+ ): Promise<{ embeddings: number[][] }> {
429
+ const {
430
+ userId,
431
+ threadId,
432
+ values,
433
+ abortSignal,
434
+ headers,
435
+ agentName,
436
+ usageHandler,
437
+ textEmbeddingModel,
438
+ embeddingModel,
439
+ callSettings,
440
+ } = args;
441
+ const effectiveEmbeddingModel = embeddingModel ?? textEmbeddingModel;
442
+ assert(
443
+ effectiveEmbeddingModel,
444
+ "an embeddingModel (or textEmbeddingModel) is required to be set for vector search",
445
+ );
446
+ const result = await embedMany_({
447
+ ...callSettings,
448
+ model: effectiveEmbeddingModel,
449
+ values,
450
+ abortSignal,
451
+ headers,
452
+ });
453
+ if (usageHandler && result.usage) {
454
+ await usageHandler(ctx, {
455
+ userId,
456
+ threadId,
457
+ agentName,
458
+ model: getModelName(effectiveEmbeddingModel),
459
+ provider: getProviderName(effectiveEmbeddingModel),
460
+ providerMetadata: undefined,
461
+ usage: {
462
+ inputTokens: result.usage.tokens,
463
+ outputTokens: 0,
464
+ totalTokens: result.usage.tokens,
465
+ // These detail fields are required by LanguageModelUsage type but we don't
466
+ // have the granular data, so we provide objects with undefined values.
467
+ inputTokenDetails: {
468
+ cacheReadTokens: undefined,
469
+ cacheWriteTokens: undefined,
470
+ noCacheTokens: undefined,
471
+ },
472
+ outputTokenDetails: {
473
+ textTokens: undefined,
474
+ reasoningTokens: undefined,
475
+ },
476
+ },
477
+ });
478
+ }
479
+ return { embeddings: result.embeddings };
480
+ }
481
+
482
+ /**
483
+ * Embed a list of messages, and save the embeddings to the database.
484
+ * @param ctx The ctx parameter to an action.
485
+ * @param component The agent component, usually components.agent.
486
+ * @param args The context for the embedding, passed to the usage handler.
487
+ * @param messages The messages to embed, in the Agent MessageDoc format.
488
+ */
489
+ export async function generateAndSaveEmbeddings(
490
+ ctx: ActionCtx,
491
+ component: AgentComponent,
492
+ args: {
493
+ threadId: string | undefined;
494
+ userId: string | undefined;
495
+ agentName?: string;
496
+ /**
497
+ * @deprecated Use embeddingModel instead.
498
+ */
499
+ textEmbeddingModel?: EmbeddingModel;
500
+ embeddingModel?: EmbeddingModel;
501
+ } & Pick<Config, "usageHandler" | "callSettings">,
502
+ messages: MessageDoc[],
503
+ ) {
504
+ const effectiveEmbeddingModel =
505
+ args.embeddingModel ?? args.textEmbeddingModel;
506
+ if (!effectiveEmbeddingModel) {
507
+ throw new Error(
508
+ "an embeddingModel (or textEmbeddingModel) is required to generate and save embeddings",
509
+ );
510
+ }
511
+ const toEmbed = messages.filter((m) => !m.embeddingId && m.message);
512
+ if (toEmbed.length === 0) {
513
+ return;
514
+ }
515
+ const embeddings = await embedMessages(
516
+ ctx,
517
+ { ...args, embeddingModel: effectiveEmbeddingModel },
518
+ toEmbed.map((m) => m.message!),
519
+ );
520
+ if (embeddings && embeddings.vectors.some((v) => v !== null)) {
521
+ await ctx.runMutation(component.vector.index.insertBatch, {
522
+ vectorDimension: embeddings.dimension,
523
+ vectors: toEmbed
524
+ .map((m, i) => ({
525
+ messageId: m._id,
526
+ model: embeddings.model,
527
+ table: "messages",
528
+ userId: m.userId,
529
+ threadId: m.threadId,
530
+ vector: embeddings.vectors[i]!,
531
+ }))
532
+ .filter((v) => v.vector !== null),
533
+ });
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Similar to fetchContextMessages, but also combines the input messages,
539
+ * with search context, recent messages, input messages, then prompt messages.
540
+ * If there is a promptMessageId and prompt message(s) provided, it will splice
541
+ * the prompt messages into the history to replace the promptMessageId message,
542
+ * but still be followed by any existing messages that were in response to the
543
+ * promptMessageId message.
544
+ */
545
+ export async function fetchContextWithPrompt(
546
+ ctx: ActionCtx,
547
+ component: AgentComponent,
548
+ args: {
549
+ prompt: string | (ModelMessage | Message)[] | undefined;
550
+ messages: (ModelMessage | Message)[] | undefined;
551
+ promptMessageId: string | undefined;
552
+ userId: string | undefined;
553
+ threadId: string | undefined;
554
+ agentName?: string;
555
+ } & Options &
556
+ Config,
557
+ ): Promise<{
558
+ messages: ModelMessage[];
559
+ order: number | undefined;
560
+ stepOrder: number | undefined;
561
+ }> {
562
+ const { threadId, userId, textEmbeddingModel, embeddingModel } = args;
563
+ const effectiveEmbeddingModel = embeddingModel ?? textEmbeddingModel;
564
+
565
+ const promptArray = getPromptArray(args.prompt);
566
+
567
+ const searchText = promptArray.length
568
+ ? extractText(promptArray.at(-1)!)
569
+ : args.promptMessageId
570
+ ? undefined
571
+ : args.messages?.at(-1)
572
+ ? extractText(args.messages.at(-1)!)
573
+ : undefined;
574
+ // If only a messageId is provided, this will add that message to the end.
575
+ const { recentMessages, searchMessages } = await fetchRecentAndSearchMessages(
576
+ ctx,
577
+ component,
578
+ {
579
+ userId,
580
+ threadId,
581
+ targetMessageId: args.promptMessageId,
582
+ searchText,
583
+ contextOptions: args.contextOptions ?? {},
584
+ getEmbedding: async (text) => {
585
+ assert(
586
+ effectiveEmbeddingModel,
587
+ "An embeddingModel (or textEmbeddingModel) is required to be set on the Agent that you're doing vector search with",
588
+ );
589
+ return {
590
+ embedding: (
591
+ await embedMany(ctx, {
592
+ ...args,
593
+ userId,
594
+ values: [text],
595
+ embeddingModel: effectiveEmbeddingModel,
596
+ })
597
+ ).embeddings[0],
598
+ embeddingModel: effectiveEmbeddingModel,
599
+ };
600
+ },
601
+ },
602
+ );
603
+
604
+ const promptMessageIndex = args.promptMessageId
605
+ ? recentMessages.findIndex((m) => m._id === args.promptMessageId)
606
+ : -1;
607
+ const promptMessage =
608
+ promptMessageIndex !== -1 ? recentMessages[promptMessageIndex] : undefined;
609
+ let prePromptDocs = recentMessages;
610
+ const messages = args.messages ?? [];
611
+ let existingResponseDocs: MessageDoc[] = [];
612
+ if (promptMessage) {
613
+ prePromptDocs = recentMessages.slice(0, promptMessageIndex);
614
+ existingResponseDocs = recentMessages.slice(promptMessageIndex + 1);
615
+ if (promptArray.length === 0) {
616
+ // If they didn't override the prompt, use the existing prompt message.
617
+ if (promptMessage.message) {
618
+ promptArray.push(promptMessage.message);
619
+ }
620
+ }
621
+ if (!promptMessage.embeddingId && effectiveEmbeddingModel) {
622
+ // Lazily generate embeddings for the prompt message, if it doesn't have
623
+ // embeddings yet. This can happen if the message was saved in a mutation
624
+ // where the LLM is not available.
625
+ await generateAndSaveEmbeddings(
626
+ ctx,
627
+ component,
628
+ {
629
+ ...args,
630
+ userId,
631
+ embeddingModel: effectiveEmbeddingModel,
632
+ },
633
+ [promptMessage],
634
+ );
635
+ }
636
+ }
637
+
638
+ const search = docsToModelMessages(searchMessages);
639
+ const recent = docsToModelMessages(prePromptDocs);
640
+ const inputMessages = messages.map(toModelMessage);
641
+ const inputPrompt = promptArray.map(toModelMessage);
642
+ const existingResponses = docsToModelMessages(existingResponseDocs);
643
+
644
+ const allMessages = mergeApprovalResponseMessages([
645
+ ...search,
646
+ ...recent,
647
+ ...inputMessages,
648
+ ...inputPrompt,
649
+ ...existingResponses,
650
+ ]);
651
+ let processedMessages = args.contextHandler
652
+ ? await args.contextHandler(ctx, {
653
+ allMessages,
654
+ search,
655
+ recent,
656
+ inputMessages,
657
+ inputPrompt,
658
+ existingResponses,
659
+ userId,
660
+ threadId,
661
+ })
662
+ : allMessages;
663
+
664
+ // Process messages to inline localhost files (if not, file urls pointing to localhost will be sent to LLM providers)
665
+ if (process.env.CONVEX_CLOUD_URL?.startsWith("http://127.0.0.1")) {
666
+ processedMessages = await inlineMessagesFiles(processedMessages);
667
+ }
668
+
669
+ return {
670
+ messages: processedMessages,
671
+ order: promptMessage?.order,
672
+ stepOrder: promptMessage?.stepOrder,
673
+ };
674
+ }
675
+
676
+ export function getPromptArray(
677
+ prompt: string | (ModelMessage | Message)[] | undefined,
678
+ ): (ModelMessage | Message)[] {
679
+ return !prompt
680
+ ? []
681
+ : Array.isArray(prompt)
682
+ ? prompt
683
+ : [{ role: "user", content: prompt }];
684
+ }