@convex-dev/agent 0.1.14 → 0.1.15-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 (62) hide show
  1. package/dist/client/index.d.ts +496 -174
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +120 -165
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/listMessages.d.ts +22 -0
  6. package/dist/client/listMessages.d.ts.map +1 -0
  7. package/dist/client/listMessages.js +25 -0
  8. package/dist/client/listMessages.js.map +1 -0
  9. package/dist/client/search.d.ts +162 -0
  10. package/dist/client/search.d.ts.map +1 -0
  11. package/dist/client/search.js +113 -0
  12. package/dist/client/search.js.map +1 -0
  13. package/dist/client/streaming.d.ts +17 -3
  14. package/dist/client/streaming.d.ts.map +1 -1
  15. package/dist/client/streaming.js +39 -1
  16. package/dist/client/streaming.js.map +1 -1
  17. package/dist/client/types.d.ts +5 -1
  18. package/dist/client/types.d.ts.map +1 -1
  19. package/dist/component/_generated/api.d.ts +14 -3
  20. package/dist/component/messages.d.ts +4 -4
  21. package/dist/component/messages.d.ts.map +1 -1
  22. package/dist/component/messages.js +12 -7
  23. package/dist/component/messages.js.map +1 -1
  24. package/dist/component/schema.d.ts +38 -28
  25. package/dist/component/schema.d.ts.map +1 -1
  26. package/dist/component/schema.js +3 -2
  27. package/dist/component/schema.js.map +1 -1
  28. package/dist/component/streams.d.ts +7 -0
  29. package/dist/component/streams.d.ts.map +1 -1
  30. package/dist/component/streams.js +49 -22
  31. package/dist/component/streams.js.map +1 -1
  32. package/dist/component/vector/index.js +3 -3
  33. package/dist/component/vector/index.js.map +1 -1
  34. package/dist/react/deltas.d.ts.map +1 -1
  35. package/dist/react/deltas.js +2 -2
  36. package/dist/react/deltas.js.map +1 -1
  37. package/dist/react/index.d.ts +4 -2
  38. package/dist/react/index.d.ts.map +1 -1
  39. package/dist/react/index.js +22 -9
  40. package/dist/react/index.js.map +1 -1
  41. package/dist/react/optimisticallySendMessage.js +1 -1
  42. package/dist/react/optimisticallySendMessage.js.map +1 -1
  43. package/dist/validators.d.ts +49 -55
  44. package/dist/validators.d.ts.map +1 -1
  45. package/dist/validators.js +3 -8
  46. package/dist/validators.js.map +1 -1
  47. package/package.json +4 -3
  48. package/src/client/index.ts +249 -268
  49. package/src/client/listMessages.ts +38 -0
  50. package/src/client/search.ts +172 -0
  51. package/src/client/streaming.ts +56 -3
  52. package/src/client/types.ts +5 -1
  53. package/src/component/_generated/api.d.ts +14 -3
  54. package/src/component/messages.ts +13 -7
  55. package/src/component/schema.ts +3 -2
  56. package/src/component/streams.ts +85 -39
  57. package/src/component/vector/index.ts +4 -4
  58. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  59. package/src/react/deltas.ts +4 -2
  60. package/src/react/index.ts +23 -11
  61. package/src/react/optimisticallySendMessage.ts +1 -1
  62. package/src/validators.ts +8 -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
  /**
@@ -96,7 +143,13 @@ export class DeltaStreamer {
96
143
  this.#nextStepOrder = (metadata.stepOrder ?? 0) + 1;
97
144
  this.abortController = new AbortController();
98
145
  if (metadata.abortSignal) {
99
- metadata.abortSignal.addEventListener("abort", () => {
146
+ metadata.abortSignal.addEventListener("abort", async () => {
147
+ if (this.streamId) {
148
+ await this.ctx.runMutation(this.component.streams.abort, {
149
+ streamId: this.streamId,
150
+ reason: "abortSignal",
151
+ });
152
+ }
100
153
  this.abortController.abort();
101
154
  });
102
155
  }
@@ -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<{
@@ -1433,6 +1433,12 @@ export type Mounts = {
1433
1433
  >;
1434
1434
  };
1435
1435
  streams: {
1436
+ abort: FunctionReference<
1437
+ "mutation",
1438
+ "public",
1439
+ { reason: string; streamId: string },
1440
+ null
1441
+ >;
1436
1442
  addDelta: FunctionReference<
1437
1443
  "mutation",
1438
1444
  "public",
@@ -1588,13 +1594,18 @@ export type Mounts = {
1588
1594
  list: FunctionReference<
1589
1595
  "query",
1590
1596
  "public",
1591
- { threadId: string },
1597
+ {
1598
+ startOrder?: number;
1599
+ statuses?: Array<"streaming" | "finished" | "aborted">;
1600
+ threadId: string;
1601
+ },
1592
1602
  Array<{
1593
1603
  agentName?: string;
1594
1604
  model?: string;
1595
1605
  order: number;
1596
1606
  provider?: string;
1597
1607
  providerOptions?: Record<string, Record<string, any>>;
1608
+ status: "streaming" | "finished" | "aborted";
1598
1609
  stepOrder: number;
1599
1610
  streamId: string;
1600
1611
  userId?: string;
@@ -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,
@@ -113,10 +113,11 @@ export const schema = defineSchema({
113
113
  v.object({
114
114
  kind: v.literal("finished"),
115
115
  endedAt: v.number(),
116
+ cleanupFnId: v.optional(v.id("_scheduled_functions")),
116
117
  }),
117
118
  v.object({
118
- kind: v.literal("error"),
119
- error: v.string(),
119
+ kind: v.literal("aborted"),
120
+ reason: v.string(),
120
121
  })
121
122
  ),
122
123
  })
@@ -6,7 +6,7 @@ import {
6
6
  vStreamMessage,
7
7
  } from "../validators.js";
8
8
  import { api, internal } from "./_generated/api.js";
9
- import type { Id } from "./_generated/dataModel.js";
9
+ import type { Doc, Id } from "./_generated/dataModel.js";
10
10
  import {
11
11
  internalMutation,
12
12
  mutation,
@@ -106,33 +106,87 @@ export const create = mutation({
106
106
  export const list = query({
107
107
  args: {
108
108
  threadId: v.id("threads"),
109
+ startOrder: v.optional(v.number()),
110
+ statuses: v.optional(
111
+ v.array(
112
+ v.union(
113
+ v.literal("streaming"),
114
+ v.literal("finished"),
115
+ v.literal("aborted")
116
+ )
117
+ )
118
+ ),
109
119
  },
110
120
  returns: v.array(vStreamMessage),
111
121
  handler: async (ctx, args) => {
112
- return ctx.db
113
- .query("streamingMessages")
114
- .withIndex("threadId_state_order_stepOrder", (q) =>
115
- q.eq("threadId", args.threadId).eq("state.kind", "streaming")
116
- )
117
- .order("desc")
118
- .take(100)
119
- .then((msgs) =>
120
- msgs.map((m) => ({
121
- streamId: m._id,
122
- ...pick(m, [
123
- "order",
124
- "stepOrder",
125
- "userId",
126
- "agentName",
127
- "model",
128
- "provider",
129
- "providerOptions",
130
- ]),
131
- }))
122
+ const statuses = args.statuses ?? ["streaming"];
123
+ const messages = await mergedStream(
124
+ statuses.map((status) =>
125
+ stream(ctx.db, schema)
126
+ .query("streamingMessages")
127
+ .withIndex("threadId_state_order_stepOrder", (q) =>
128
+ q
129
+ .eq("threadId", args.threadId)
130
+ .eq("state.kind", status)
131
+ .gte("order", args.startOrder ?? 0)
132
+ )
133
+ .order("desc")
134
+ ),
135
+ ["order", "stepOrder"]
136
+ ).take(100);
137
+
138
+ return messages.map((m) => ({
139
+ streamId: m._id,
140
+ status: m.state.kind,
141
+ ...pick(m, [
142
+ "order",
143
+ "stepOrder",
144
+ "userId",
145
+ "agentName",
146
+ "model",
147
+ "provider",
148
+ "providerOptions",
149
+ ]),
150
+ }));
151
+ },
152
+ });
153
+
154
+ export const abort = mutation({
155
+ args: {
156
+ streamId: v.id("streamingMessages"),
157
+ reason: v.string(),
158
+ },
159
+ returns: v.null(),
160
+ handler: async (ctx, args) => {
161
+ const stream = await ctx.db.get(args.streamId);
162
+ if (!stream) {
163
+ throw new Error(`Stream not found: ${args.streamId}`);
164
+ }
165
+ if (stream.state.kind !== "streaming") {
166
+ console.warn(
167
+ `Stream trying to abort but not currently streaming (${stream.state.kind}): ${args.streamId}`
132
168
  );
169
+ return;
170
+ }
171
+ await cleanupTimeoutFn(ctx, stream);
172
+ await ctx.db.patch(args.streamId, {
173
+ state: { kind: "aborted", reason: args.reason },
174
+ });
133
175
  },
134
176
  });
135
177
 
178
+ async function cleanupTimeoutFn(
179
+ ctx: MutationCtx,
180
+ stream: Doc<"streamingMessages">
181
+ ) {
182
+ if (stream.state.kind === "streaming" && stream.state.timeoutFnId) {
183
+ const timeoutFn = await ctx.db.system.get(stream.state.timeoutFnId);
184
+ if (timeoutFn?.state.kind === "pending") {
185
+ await ctx.scheduler.cancel(stream.state.timeoutFnId);
186
+ }
187
+ }
188
+ }
189
+
136
190
  export const finish = mutation({
137
191
  args: {
138
192
  streamId: v.id("streamingMessages"),
@@ -153,20 +207,15 @@ export const finish = mutation({
153
207
  );
154
208
  return;
155
209
  }
156
- if (stream.state.timeoutFnId) {
157
- const timeoutFn = await ctx.db.system.get(stream.state.timeoutFnId);
158
- if (timeoutFn?.state.kind === "pending") {
159
- await ctx.scheduler.cancel(stream.state.timeoutFnId);
160
- }
161
- }
162
- await ctx.db.patch(args.streamId, {
163
- state: { kind: "finished", endedAt: Date.now() },
164
- });
165
- await ctx.scheduler.runAfter(
210
+ await cleanupTimeoutFn(ctx, stream);
211
+ const cleanupFnId = await ctx.scheduler.runAfter(
166
212
  DELETE_STREAM_DELAY,
167
213
  api.streams.deleteStreamAsync,
168
214
  { streamId: args.streamId }
169
215
  );
216
+ await ctx.db.patch(args.streamId, {
217
+ state: { kind: "finished", endedAt: Date.now(), cleanupFnId },
218
+ });
170
219
  },
171
220
  });
172
221
 
@@ -223,8 +272,8 @@ export const timeoutStream = internalMutation({
223
272
  }
224
273
  await ctx.db.patch(args.streamId, {
225
274
  state: {
226
- kind: "finished",
227
- endedAt: Date.now(),
275
+ kind: "aborted",
276
+ reason: "timeout",
228
277
  },
229
278
  });
230
279
  },
@@ -245,12 +294,9 @@ async function deletePageForStreamId(
245
294
  if (deltas.isDone) {
246
295
  const stream = await ctx.db.get(args.streamId);
247
296
  if (stream) {
248
- const state = stream.state;
249
- if (state.kind === "streaming" && state.timeoutFnId) {
250
- const timeoutFn = await ctx.db.system.get(state.timeoutFnId);
251
- if (timeoutFn?.state.kind === "pending") {
252
- await ctx.scheduler.cancel(state.timeoutFnId);
253
- }
297
+ await cleanupTimeoutFn(ctx, stream);
298
+ if (stream.state.kind === "finished" && stream.state.cleanupFnId) {
299
+ await ctx.scheduler.cancel(stream.state.cleanupFnId);
254
300
  }
255
301
  await ctx.db.delete(args.streamId);
256
302
  }
@@ -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
  );
@@ -1 +1 @@
1
- {"version":"3.2.4","results":[[":client/index.test.ts",{"duration":60.55858325958252,"failed":false}],[":component/messages.test.ts",{"duration":27.41837501525879,"failed":false}]]}
1
+ {"version":"3.2.4","results":[[":client/index.test.ts",{"duration":75.65995979309082,"failed":false}],[":component/messages.test.ts",{"duration":43.858792304992676,"failed":false}],[":react/toUIMessages.test.ts",{"duration":4.371167182922363,"failed":false}]]}
@@ -256,12 +256,14 @@ export function createStreamingMessage(
256
256
  ): MessageDoc {
257
257
  const { streamId, ...rest } = message;
258
258
  const metadata: MessageDoc = {
259
+ ...rest,
259
260
  _id: `${streamId}-${index}`,
260
261
  _creationTime: Date.now(),
261
- status: "pending",
262
+ status: (
263
+ { streaming: "pending", finished: "success", aborted: "failed" } as const
264
+ )[message.status],
262
265
  threadId,
263
266
  tool: false,
264
- ...rest,
265
267
  };
266
268
  switch (part.type) {
267
269
  case "text-delta":