@convex-dev/agent 0.1.18 → 0.2.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 (122) hide show
  1. package/dist/client/createTool.d.ts +30 -11
  2. package/dist/client/createTool.d.ts.map +1 -1
  3. package/dist/client/createTool.js +24 -10
  4. package/dist/client/createTool.js.map +1 -1
  5. package/dist/client/defaultComponent.d.ts +11 -0
  6. package/dist/client/defaultComponent.d.ts.map +1 -0
  7. package/dist/client/defaultComponent.js +7 -0
  8. package/dist/client/defaultComponent.js.map +1 -0
  9. package/dist/client/definePlaygroundAPI.d.ts +68 -23
  10. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  11. package/dist/client/definePlaygroundAPI.js +7 -20
  12. package/dist/client/definePlaygroundAPI.js.map +1 -1
  13. package/dist/client/files.d.ts +14 -4
  14. package/dist/client/files.d.ts.map +1 -1
  15. package/dist/client/files.js +66 -11
  16. package/dist/client/files.js.map +1 -1
  17. package/dist/client/index.d.ts +323 -780
  18. package/dist/client/index.d.ts.map +1 -1
  19. package/dist/client/index.js +445 -543
  20. package/dist/client/index.js.map +1 -1
  21. package/dist/client/messages.d.ts +256 -0
  22. package/dist/client/messages.d.ts.map +1 -0
  23. package/dist/client/messages.js +97 -0
  24. package/dist/client/messages.js.map +1 -0
  25. package/dist/client/search.d.ts +35 -12
  26. package/dist/client/search.d.ts.map +1 -1
  27. package/dist/client/search.js +21 -4
  28. package/dist/client/search.js.map +1 -1
  29. package/dist/client/streaming.d.ts +21 -21
  30. package/dist/client/streaming.d.ts.map +1 -1
  31. package/dist/client/streaming.js +35 -40
  32. package/dist/client/streaming.js.map +1 -1
  33. package/dist/client/threads.d.ts +46 -0
  34. package/dist/client/threads.d.ts.map +1 -0
  35. package/dist/client/threads.js +49 -0
  36. package/dist/client/threads.js.map +1 -0
  37. package/dist/client/types.d.ts +159 -75
  38. package/dist/client/types.d.ts.map +1 -1
  39. package/dist/component/_generated/api.d.ts +261 -209
  40. package/dist/component/messages.d.ts +314 -105
  41. package/dist/component/messages.d.ts.map +1 -1
  42. package/dist/component/messages.js +141 -83
  43. package/dist/component/messages.js.map +1 -1
  44. package/dist/component/schema.d.ts +631 -870
  45. package/dist/component/schema.d.ts.map +1 -1
  46. package/dist/component/schema.js +11 -16
  47. package/dist/component/schema.js.map +1 -1
  48. package/dist/component/streams.d.ts +29 -149
  49. package/dist/component/streams.d.ts.map +1 -1
  50. package/dist/component/streams.js +104 -60
  51. package/dist/component/streams.js.map +1 -1
  52. package/dist/component/threads.d.ts +4 -4
  53. package/dist/component/users.d.ts +3 -3
  54. package/dist/mapping.d.ts +15 -16
  55. package/dist/mapping.d.ts.map +1 -1
  56. package/dist/mapping.js +208 -62
  57. package/dist/mapping.js.map +1 -1
  58. package/dist/parts.d.ts +3 -0
  59. package/dist/parts.d.ts.map +1 -0
  60. package/dist/parts.js +34 -0
  61. package/dist/parts.js.map +1 -0
  62. package/dist/react/SmoothText.d.ts +5 -0
  63. package/dist/react/SmoothText.d.ts.map +1 -0
  64. package/dist/react/SmoothText.js +6 -0
  65. package/dist/react/SmoothText.js.map +1 -0
  66. package/dist/react/deltas.d.ts +3 -5
  67. package/dist/react/deltas.d.ts.map +1 -1
  68. package/dist/react/deltas.js +89 -53
  69. package/dist/react/deltas.js.map +1 -1
  70. package/dist/react/index.d.ts +1 -0
  71. package/dist/react/index.d.ts.map +1 -1
  72. package/dist/react/index.js +31 -11
  73. package/dist/react/index.js.map +1 -1
  74. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  75. package/dist/react/optimisticallySendMessage.js +2 -8
  76. package/dist/react/optimisticallySendMessage.js.map +1 -1
  77. package/dist/react/toUIMessages.d.ts +5 -4
  78. package/dist/react/toUIMessages.d.ts.map +1 -1
  79. package/dist/react/toUIMessages.js +103 -40
  80. package/dist/react/toUIMessages.js.map +1 -1
  81. package/dist/react/useSmoothText.d.ts +13 -12
  82. package/dist/react/useSmoothText.d.ts.map +1 -1
  83. package/dist/react/useSmoothText.js +1 -1
  84. package/dist/react/useSmoothText.js.map +1 -1
  85. package/dist/validators.d.ts +751 -1464
  86. package/dist/validators.d.ts.map +1 -1
  87. package/dist/validators.js +39 -80
  88. package/dist/validators.js.map +1 -1
  89. package/package.json +34 -30
  90. package/src/client/createTool.ts +66 -40
  91. package/src/client/defaultComponent.ts +17 -0
  92. package/src/client/definePlaygroundAPI.ts +17 -31
  93. package/src/client/files.ts +94 -17
  94. package/src/client/index.test.ts +28 -38
  95. package/src/client/index.ts +785 -977
  96. package/src/client/messages.ts +205 -0
  97. package/src/client/search.ts +37 -10
  98. package/src/client/streaming.ts +53 -56
  99. package/src/client/threads.ts +78 -0
  100. package/src/client/types.ts +239 -131
  101. package/src/component/_generated/api.d.ts +261 -209
  102. package/src/component/messages.test.ts +182 -40
  103. package/src/component/messages.ts +165 -108
  104. package/src/component/schema.ts +11 -17
  105. package/src/component/streams.ts +180 -69
  106. package/src/mapping.ts +253 -96
  107. package/src/parts.ts +39 -0
  108. package/src/react/SmoothText.tsx +9 -0
  109. package/src/react/deltas.test.ts +38 -42
  110. package/src/react/deltas.ts +116 -65
  111. package/src/react/index.ts +35 -13
  112. package/src/react/optimisticallySendMessage.ts +2 -8
  113. package/src/react/toUIMessages.test.ts +154 -36
  114. package/src/react/toUIMessages.ts +136 -57
  115. package/src/react/useSmoothText.ts +14 -15
  116. package/src/validators.test.ts +1 -117
  117. package/src/validators.ts +57 -107
  118. package/dist/client/listMessages.d.ts +0 -22
  119. package/dist/client/listMessages.d.ts.map +0 -1
  120. package/dist/client/listMessages.js +0 -25
  121. package/dist/client/listMessages.js.map +0 -1
  122. package/src/client/listMessages.ts +0 -38
@@ -0,0 +1,205 @@
1
+ import type { ModelMessage } from "ai";
2
+ import type { PaginationOptions, PaginationResult } from "convex/server";
3
+ import type { MessageDoc } from "../component/schema.js";
4
+ import { validateVectorDimension } from "../component/vector/tables.js";
5
+ import {
6
+ vMessageWithMetadata,
7
+ type Message,
8
+ type MessageEmbeddings,
9
+ type MessageEmbeddingsWithDimension,
10
+ type MessageStatus,
11
+ type MessageWithMetadata,
12
+ } from "../validators.js";
13
+ import { serializeMessage } from "./index.js";
14
+ import type { AgentComponent, RunMutationCtx, RunQueryCtx } from "./types.js";
15
+ import { parse } from "convex-helpers/validators";
16
+
17
+ /**
18
+ * List messages from a thread.
19
+ * @param ctx A ctx object from a query, mutation, or action.
20
+ * @param component The agent component, usually `components.agent`.
21
+ * @param args.threadId The thread to list messages from.
22
+ * @param args.paginationOpts Pagination options (e.g. via usePaginatedQuery).
23
+ * @param args.excludeToolMessages Whether to exclude tool messages.
24
+ * False by default.
25
+ * @param args.statuses What statuses to include. All by default.
26
+ * @returns The MessageDoc's in a format compatible with usePaginatedQuery.
27
+ */
28
+ export async function listMessages(
29
+ ctx: RunQueryCtx,
30
+ component: AgentComponent,
31
+ args: {
32
+ threadId: string;
33
+ paginationOpts: PaginationOptions;
34
+ excludeToolMessages?: boolean;
35
+ statuses?: MessageStatus[];
36
+ },
37
+ ): Promise<PaginationResult<MessageDoc>> {
38
+ if (args.paginationOpts.numItems === 0) {
39
+ return {
40
+ page: [],
41
+ isDone: true,
42
+ continueCursor: args.paginationOpts.cursor ?? "",
43
+ };
44
+ }
45
+ return ctx.runQuery(component.messages.listMessagesByThreadId, {
46
+ order: "desc",
47
+ ...args,
48
+ });
49
+ }
50
+
51
+ export type SaveMessagesArgs = {
52
+ threadId: string;
53
+ userId?: string | null;
54
+ /**
55
+ * The message that these messages are in response to. They will be
56
+ * the same "order" as this message, at increasing stepOrder(s).
57
+ */
58
+ promptMessageId?: string;
59
+ /**
60
+ * The messages to save.
61
+ */
62
+ messages: (ModelMessage | Message)[];
63
+ /**
64
+ * Metadata to save with the messages. Each element corresponds to the
65
+ * message at the same index.
66
+ */
67
+ metadata?: Omit<MessageWithMetadata, "message">[];
68
+ /**
69
+ * If true, it will fail any pending steps.
70
+ * Defaults to false.
71
+ */
72
+ failPendingSteps?: boolean;
73
+ /**
74
+ * The embeddings to save with the messages.
75
+ */
76
+ embeddings?: MessageEmbeddings;
77
+ /**
78
+ * A pending message ID to replace when adding messages.
79
+ */
80
+ pendingMessageId?: string;
81
+ };
82
+
83
+ /**
84
+ * Explicitly save messages associated with the thread (& user if provided)
85
+ */
86
+ export async function saveMessages(
87
+ ctx: RunMutationCtx,
88
+ component: AgentComponent,
89
+ args: SaveMessagesArgs & {
90
+ /**
91
+ * The agent name to associate with the messages.
92
+ */
93
+ agentName?: string;
94
+ },
95
+ ): Promise<{ messages: MessageDoc[] }> {
96
+ let embeddings: MessageEmbeddingsWithDimension | undefined;
97
+ if (args.embeddings) {
98
+ const dimension = args.embeddings.vectors.find((v) => v !== null)?.length;
99
+ if (dimension) {
100
+ validateVectorDimension(dimension);
101
+ embeddings = {
102
+ model: args.embeddings.model,
103
+ dimension,
104
+ vectors: args.embeddings.vectors,
105
+ };
106
+ }
107
+ }
108
+ const result = await ctx.runMutation(component.messages.addMessages, {
109
+ threadId: args.threadId,
110
+ userId: args.userId ?? undefined,
111
+ agentName: args.agentName,
112
+ promptMessageId: args.promptMessageId,
113
+ pendingMessageId: args.pendingMessageId,
114
+ embeddings,
115
+ messages: await Promise.all(
116
+ args.messages.map(async (m, i) => {
117
+ const { message, fileIds } = await serializeMessage(ctx, component, m);
118
+ const allFileIds = args.metadata?.[i]?.fileIds ?? [];
119
+ if (fileIds) {
120
+ allFileIds.push(...fileIds);
121
+ }
122
+ return parse(vMessageWithMetadata, {
123
+ ...args.metadata?.[i],
124
+ message,
125
+ fileIds: allFileIds.length > 0 ? allFileIds : undefined,
126
+ });
127
+ }),
128
+ ),
129
+ failPendingSteps: args.failPendingSteps ?? false,
130
+ });
131
+ return { messages: result.messages };
132
+ }
133
+
134
+ export type SaveMessageArgs = {
135
+ threadId: string;
136
+ userId?: string | null;
137
+ /**
138
+ * Metadata to save with the messages. Each element corresponds to the
139
+ * message at the same index.
140
+ */
141
+ metadata?: Omit<MessageWithMetadata, "message">;
142
+ /**
143
+ * The embedding to save with the message.
144
+ */
145
+ embedding?: { vector: number[]; model: string };
146
+ /**
147
+ * A pending message ID to replace with this message.
148
+ */
149
+ pendingMessageId?: string;
150
+ } & (
151
+ | {
152
+ prompt?: undefined;
153
+ /**
154
+ * The message to save.
155
+ */
156
+ message: ModelMessage | Message;
157
+ }
158
+ | {
159
+ /*
160
+ * The prompt to save with the message.
161
+ */
162
+ prompt: string;
163
+ message?: undefined;
164
+ }
165
+ );
166
+
167
+ /**
168
+ * Save a message to the thread.
169
+ * @param ctx A ctx object from a mutation or action.
170
+ * @param args The message and what to associate it with (user / thread)
171
+ * You can pass extra metadata alongside the message, e.g. associated fileIds.
172
+ * @returns The messageId of the saved message.
173
+ */
174
+ export async function saveMessage(
175
+ ctx: RunMutationCtx,
176
+ component: AgentComponent,
177
+ args: SaveMessageArgs & {
178
+ /**
179
+ * The agent name to associate with the message.
180
+ */
181
+ agentName?: string;
182
+ },
183
+ ) {
184
+ let embeddings: { vectors: number[][]; model: string } | undefined;
185
+ if (args.embedding && args.embedding.vector) {
186
+ embeddings = {
187
+ model: args.embedding.model,
188
+ vectors: [args.embedding.vector],
189
+ };
190
+ }
191
+ const { messages } = await saveMessages(ctx, component, {
192
+ threadId: args.threadId,
193
+ userId: args.userId ?? undefined,
194
+ agentName: args.agentName,
195
+ pendingMessageId: args.pendingMessageId,
196
+ messages:
197
+ args.prompt !== undefined
198
+ ? [{ role: "user", content: args.prompt }]
199
+ : [args.message],
200
+ metadata: args.metadata ? [args.metadata] : undefined,
201
+ embeddings,
202
+ });
203
+ const message = messages.at(-1)!;
204
+ return { messageId: message._id, message };
205
+ }
@@ -5,19 +5,22 @@ import type {
5
5
  RunQueryCtx,
6
6
  } from "./types.js";
7
7
  import type { MessageDoc } from "../component/schema.js";
8
- import type { CoreMessage } from "ai";
8
+ import type { EmbeddingModel, LanguageModel, ModelMessage } from "ai";
9
9
  import { assert } from "convex-helpers";
10
10
  import {
11
11
  DEFAULT_MESSAGE_RANGE,
12
12
  DEFAULT_RECENT_MESSAGES,
13
13
  extractText,
14
14
  } from "../shared.js";
15
+ import type { Message } from "../validators.js";
15
16
 
16
17
  const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
17
18
 
18
- export type GetEmbedding = (text: string) => Promise<{
19
+ export type GetEmbedding = (
20
+ text: string,
21
+ ) => Promise<{
19
22
  embedding: number[];
20
- embeddingModel: string;
23
+ textEmbeddingModel: string | EmbeddingModel<string>;
21
24
  }>;
22
25
 
23
26
  /**
@@ -34,7 +37,7 @@ export async function fetchContextMessages(
34
37
  args: {
35
38
  userId: string | undefined;
36
39
  threadId: string | undefined;
37
- messages: CoreMessage[];
40
+ messages: (ModelMessage | Message)[];
38
41
  /**
39
42
  * If provided, it will search for messages up to and including this message.
40
43
  * Note: if this is far in the past, text and vector search results may be more
@@ -95,20 +98,21 @@ export async function fetchContextMessages(
95
98
  "You must provide an embedding and embeddingModel to use vector search",
96
99
  );
97
100
  }
98
- const embeddingFields = opts.searchOptions?.vectorSearch
99
- ? await args.getEmbedding?.(text)
100
- : undefined;
101
+ const embeddingFields =
102
+ opts.searchOptions?.vectorSearch && text
103
+ ? await args.getEmbedding?.(text)
104
+ : undefined;
101
105
  const searchMessages = await ctx.runAction(
102
106
  component.messages.searchMessages,
103
107
  {
104
108
  searchAllMessagesForUserId: opts?.searchOtherThreads
105
- ? args.userId ??
109
+ ? (args.userId ??
106
110
  (args.threadId &&
107
111
  (
108
112
  await ctx.runQuery(component.threads.getThread, {
109
113
  threadId: args.threadId,
110
114
  })
111
- )?.userId)
115
+ )?.userId))
112
116
  : undefined,
113
117
  threadId: args.threadId,
114
118
  beforeMessageId: args.upToAndIncludingMessageId,
@@ -122,7 +126,9 @@ export async function fetchContextMessages(
122
126
  opts.searchOptions?.vectorScoreThreshold ??
123
127
  DEFAULT_VECTOR_SCORE_THRESHOLD,
124
128
  embedding: embeddingFields?.embedding,
125
- embeddingModel: embeddingFields?.embeddingModel,
129
+ embeddingModel: embeddingFields?.textEmbeddingModel
130
+ ? getModelName(embeddingFields.textEmbeddingModel)
131
+ : undefined,
126
132
  },
127
133
  );
128
134
  // TODO: track what messages we used for context
@@ -170,3 +176,24 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
170
176
  }
171
177
  return result;
172
178
  }
179
+
180
+ export function getModelName(
181
+ embeddingModel: string | EmbeddingModel<string> | LanguageModel,
182
+ ): string {
183
+ if (typeof embeddingModel === "string") {
184
+ if (embeddingModel.includes("/")) {
185
+ return embeddingModel.split("/").slice(1).join("/");
186
+ }
187
+ return embeddingModel;
188
+ }
189
+ return embeddingModel.modelId;
190
+ }
191
+
192
+ export function getProviderName(
193
+ embeddingModel: string | EmbeddingModel<string> | LanguageModel,
194
+ ): string {
195
+ if (typeof embeddingModel === "string") {
196
+ return embeddingModel.split("/").at(0)!;
197
+ }
198
+ return embeddingModel.provider;
199
+ }
@@ -2,6 +2,7 @@ import {
2
2
  type ChunkDetector,
3
3
  smoothStream,
4
4
  type StreamTextTransform,
5
+ type TextStreamPart,
5
6
  type ToolSet,
6
7
  } from "ai";
7
8
  import type {
@@ -9,9 +10,7 @@ import type {
9
10
  StreamArgs,
10
11
  StreamDelta,
11
12
  StreamMessage,
12
- TextStreamPart,
13
13
  } from "../validators.js";
14
- import type { MessageDoc } from "../component/schema.js";
15
14
  import type {
16
15
  AgentComponent,
17
16
  RunActionCtx,
@@ -20,6 +19,7 @@ import type {
20
19
  SyncStreamsReturnValue,
21
20
  } from "./types.js";
22
21
  import { omit } from "convex-helpers";
22
+ import { serializeTextStreamingPartsV5 } from "../parts.js";
23
23
 
24
24
  /**
25
25
  * A function that handles fetching stream deltas, used with the React hooks
@@ -64,9 +64,10 @@ export async function syncStreams(
64
64
  export async function abortStream(
65
65
  ctx: RunMutationCtx,
66
66
  component: AgentComponent,
67
- args: {
68
- reason: string;
69
- } & ({ streamId: string } | { threadId: string; order: number }),
67
+ args: { reason: string } & (
68
+ | { streamId: string }
69
+ | { threadId: string; order: number }
70
+ ),
70
71
  ): Promise<boolean> {
71
72
  if ("streamId" in args) {
72
73
  return await ctx.runMutation(component.streams.abort, {
@@ -158,9 +159,7 @@ export function mergeTransforms<TOOLS extends ToolSet>(
158
159
  export class DeltaStreamer {
159
160
  public streamId: string | undefined;
160
161
  public readonly options: Required<StreamingOptions>;
161
- #nextParts: TextStreamPart[] = [];
162
- #nextOrder: number;
163
- #nextStepOrder: number;
162
+ #nextParts: TextStreamPart<ToolSet>[] = [];
164
163
  #latestWrite: number = 0;
165
164
  #ongoingWrite: Promise<void> | undefined;
166
165
  #cursor: number = 0;
@@ -172,51 +171,46 @@ export class DeltaStreamer {
172
171
  options: true | StreamingOptions,
173
172
  public readonly metadata: {
174
173
  threadId: string;
175
- agentName: string | undefined;
176
- model: string | undefined;
177
- provider: string | undefined;
178
- providerOptions: ProviderOptions | undefined;
179
- userId: string | undefined;
180
- order: number | undefined;
181
- stepOrder: number | undefined;
182
- abortSignal: AbortSignal | undefined;
174
+ userId?: string;
175
+ order: number;
176
+ stepOrder: number;
177
+ agentName?: string;
178
+ model?: string;
179
+ provider?: string;
180
+ providerOptions?: ProviderOptions;
181
+ abortSignal?: AbortSignal;
183
182
  },
184
183
  ) {
185
184
  this.options =
186
185
  typeof options === "boolean"
187
186
  ? DEFAULT_STREAMING_OPTIONS
188
- : {
189
- ...DEFAULT_STREAMING_OPTIONS,
190
- ...options,
191
- };
187
+ : { ...DEFAULT_STREAMING_OPTIONS, ...options };
192
188
  this.#nextParts = [];
193
- this.#nextOrder = metadata.order ?? 0;
194
- this.#nextStepOrder = (metadata.stepOrder ?? 0) + 1;
195
189
  this.abortController = new AbortController();
196
190
  if (metadata.abortSignal) {
197
191
  metadata.abortSignal.addEventListener("abort", async () => {
198
192
  if (this.streamId) {
193
+ this.abortController.abort();
194
+ const finalDelta = this.#createDelta();
195
+ await this.#ongoingWrite;
199
196
  await this.ctx.runMutation(this.component.streams.abort, {
200
197
  streamId: this.streamId,
201
198
  reason: "abortSignal",
199
+ finalDelta,
202
200
  });
203
201
  }
204
- this.abortController.abort();
205
202
  });
206
203
  }
207
204
  }
208
- public async addParts(parts: TextStreamPart[]) {
205
+
206
+ public async addParts(parts: TextStreamPart<ToolSet>[]) {
209
207
  if (this.abortController.signal.aborted) {
210
208
  return;
211
209
  }
212
210
  if (!this.streamId) {
213
211
  this.streamId = await this.ctx.runMutation(
214
212
  this.component.streams.create,
215
- {
216
- ...omit(this.metadata, ["abortSignal"]),
217
- order: this.#nextOrder,
218
- stepOrder: this.#nextStepOrder,
219
- },
213
+ omit(this.metadata, ["abortSignal"]),
220
214
  );
221
215
  }
222
216
  this.#nextParts.push(...parts);
@@ -233,6 +227,9 @@ export class DeltaStreamer {
233
227
  return;
234
228
  }
235
229
  const delta = this.#createDelta();
230
+ if (!delta) {
231
+ return;
232
+ }
236
233
  this.#latestWrite = Date.now();
237
234
  try {
238
235
  const success = await this.ctx.runMutation(
@@ -258,46 +255,46 @@ export class DeltaStreamer {
258
255
  }
259
256
  }
260
257
 
261
- #createDelta(): StreamDelta {
258
+ #createDelta(): StreamDelta | undefined {
259
+ if (this.#nextParts.length === 0) {
260
+ return undefined;
261
+ }
262
262
  const start = this.#cursor;
263
263
  const end = start + this.#nextParts.length;
264
264
  this.#cursor = end;
265
- const parts = this.#nextParts;
265
+ const parts = serializeTextStreamingPartsV5(this.#nextParts);
266
266
  this.#nextParts = [];
267
267
  if (!this.streamId) {
268
268
  throw new Error("Creating a delta before the stream is created");
269
269
  }
270
- return {
270
+ return { streamId: this.streamId, start, end, parts };
271
+ }
272
+
273
+ public async finish() {
274
+ if (!this.streamId) {
275
+ return;
276
+ }
277
+ const finalDelta = this.#createDelta();
278
+ await this.#ongoingWrite;
279
+ await this.ctx.runMutation(this.component.streams.finish, {
271
280
  streamId: this.streamId,
272
- start,
273
- end,
274
- parts,
275
- };
281
+ finalDelta,
282
+ });
276
283
  }
277
284
 
278
- public async finish(messages: MessageDoc[]) {
279
- if (this.#ongoingWrite) {
280
- await this.#ongoingWrite;
281
- this.#ongoingWrite = undefined;
285
+ public async fail(reason: string) {
286
+ if (this.abortController.signal.aborted) {
287
+ return;
282
288
  }
289
+ this.abortController.abort();
283
290
  if (!this.streamId) {
284
- throw new Error("Finish called before stream is created");
285
- }
286
- const lastMessage = messages.at(-1);
287
- if (lastMessage) {
288
- this.#nextOrder = lastMessage.order;
289
- this.#nextStepOrder = lastMessage.stepOrder + 1;
290
- } else {
291
- console.warn("Step finished without generating a message");
291
+ return;
292
292
  }
293
- const finalDelta =
294
- this.#nextParts.length > 0 ? this.#createDelta() : undefined;
295
- this.#nextParts = [];
296
- const streamId = this.streamId;
297
- this.streamId = undefined;
298
- this.#cursor = 0;
299
- await this.ctx.runMutation(this.component.streams.finish, {
300
- streamId,
293
+ const finalDelta = this.#createDelta();
294
+ await this.#ongoingWrite;
295
+ await this.ctx.runMutation(this.component.streams.abort, {
296
+ streamId: this.streamId,
297
+ reason,
301
298
  finalDelta,
302
299
  });
303
300
  }
@@ -0,0 +1,78 @@
1
+ import type { WithoutSystemFields } from "convex/server";
2
+ import type { ThreadDoc } from "../component/schema.js";
3
+ import type { AgentComponent, RunMutationCtx, RunQueryCtx } from "./types.js";
4
+
5
+ /**
6
+ * Create a thread to store messages with an Agent.
7
+ * @param ctx The context from a mutation or action.
8
+ * @param component The Agent component, usually `components.agent`.
9
+ * @param args The associated thread metadata.
10
+ * @returns The id of the created thread.
11
+ */
12
+ export async function createThread(
13
+ ctx: RunMutationCtx,
14
+ component: AgentComponent,
15
+ args?: { userId?: string | null; title?: string; summary?: string },
16
+ ) {
17
+ const { _id: threadId } = await ctx.runMutation(
18
+ component.threads.createThread,
19
+ {
20
+ userId: args?.userId ?? undefined,
21
+ title: args?.title,
22
+ summary: args?.summary,
23
+ },
24
+ );
25
+ return threadId;
26
+ }
27
+
28
+ /**
29
+ * Get the metadata for a thread.
30
+ * @param ctx A ctx object from a query, mutation, or action.
31
+ * @param args.threadId The thread to get the metadata for.
32
+ * @returns The metadata for the thread.
33
+ */
34
+ export async function getThreadMetadata(
35
+ ctx: RunQueryCtx,
36
+ component: AgentComponent,
37
+ args: { threadId: string },
38
+ ): Promise<ThreadDoc> {
39
+ const thread = await ctx.runQuery(component.threads.getThread, {
40
+ threadId: args.threadId,
41
+ });
42
+ if (!thread) {
43
+ throw new Error("Thread not found");
44
+ }
45
+ return thread;
46
+ }
47
+
48
+ export async function updateThreadMetadata(
49
+ ctx: RunMutationCtx,
50
+ component: AgentComponent,
51
+ args: { threadId: string; patch: Partial<WithoutSystemFields<ThreadDoc>> },
52
+ ) {
53
+ return ctx.runMutation(component.threads.updateThread, {
54
+ threadId: args.threadId,
55
+ patch: args.patch,
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Search for threads by title, paginated.
61
+ * @param ctx The context passed from the query/mutation/action.
62
+ * @returns The threads matching the search, paginated.
63
+ */
64
+ export async function searchThreadTitles(
65
+ ctx: RunQueryCtx,
66
+ component: AgentComponent,
67
+ {
68
+ userId,
69
+ query,
70
+ limit,
71
+ }: { userId?: string | undefined; query: string; limit?: number },
72
+ ): Promise<ThreadDoc[]> {
73
+ return ctx.runQuery(component.threads.searchThreadTitles, {
74
+ userId,
75
+ query,
76
+ limit: limit ?? 10,
77
+ });
78
+ }