@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +32 -27
  2. package/dist/UIMessages.d.ts +46 -0
  3. package/dist/UIMessages.d.ts.map +1 -0
  4. package/dist/UIMessages.js +546 -0
  5. package/dist/UIMessages.js.map +1 -0
  6. package/dist/client/createTool.d.ts +129 -27
  7. package/dist/client/createTool.d.ts.map +1 -1
  8. package/dist/client/createTool.js +66 -12
  9. package/dist/client/createTool.js.map +1 -1
  10. package/dist/client/defaultComponent.d.ts +11 -0
  11. package/dist/client/defaultComponent.d.ts.map +1 -0
  12. package/dist/client/defaultComponent.js +7 -0
  13. package/dist/client/defaultComponent.js.map +1 -0
  14. package/dist/client/definePlaygroundAPI.d.ts +1323 -192
  15. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  16. package/dist/client/definePlaygroundAPI.js +52 -28
  17. package/dist/client/definePlaygroundAPI.js.map +1 -1
  18. package/dist/client/files.d.ts +20 -7
  19. package/dist/client/files.d.ts.map +1 -1
  20. package/dist/client/files.js +68 -11
  21. package/dist/client/files.js.map +1 -1
  22. package/dist/client/index.d.ts +1056 -965
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +242 -748
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/client/messages.d.ts +461 -0
  27. package/dist/client/messages.d.ts.map +1 -0
  28. package/dist/client/messages.js +106 -0
  29. package/dist/client/messages.js.map +1 -0
  30. package/dist/client/mockModel.d.ts +42 -0
  31. package/dist/client/mockModel.d.ts.map +1 -0
  32. package/dist/client/mockModel.js +175 -0
  33. package/dist/client/mockModel.js.map +1 -0
  34. package/dist/client/saveInputMessages.d.ts +20 -0
  35. package/dist/client/saveInputMessages.d.ts.map +1 -0
  36. package/dist/client/saveInputMessages.js +58 -0
  37. package/dist/client/saveInputMessages.js.map +1 -0
  38. package/dist/client/search.d.ts +346 -35
  39. package/dist/client/search.d.ts.map +1 -1
  40. package/dist/client/search.js +350 -39
  41. package/dist/client/search.js.map +1 -1
  42. package/dist/client/start.d.ts +84 -0
  43. package/dist/client/start.d.ts.map +1 -0
  44. package/dist/client/start.js +171 -0
  45. package/dist/client/start.js.map +1 -0
  46. package/dist/client/streamText.d.ts +46 -0
  47. package/dist/client/streamText.d.ts.map +1 -0
  48. package/dist/client/streamText.js +93 -0
  49. package/dist/client/streamText.js.map +1 -0
  50. package/dist/client/streaming.d.ts +3705 -32
  51. package/dist/client/streaming.d.ts.map +1 -1
  52. package/dist/client/streaming.js +141 -59
  53. package/dist/client/streaming.js.map +1 -1
  54. package/dist/client/threads.d.ts +46 -0
  55. package/dist/client/threads.d.ts.map +1 -0
  56. package/dist/client/threads.js +49 -0
  57. package/dist/client/threads.js.map +1 -0
  58. package/dist/client/types.d.ts +265 -128
  59. package/dist/client/types.d.ts.map +1 -1
  60. package/dist/client/utils.d.ts +4 -0
  61. package/dist/client/utils.d.ts.map +1 -0
  62. package/dist/client/utils.js +21 -0
  63. package/dist/client/utils.js.map +1 -0
  64. package/dist/component/_generated/api.d.ts +24 -2178
  65. package/dist/component/_generated/api.d.ts.map +1 -1
  66. package/dist/component/_generated/api.js +10 -1
  67. package/dist/component/_generated/api.js.map +1 -1
  68. package/dist/component/_generated/component.d.ts +3119 -0
  69. package/dist/component/_generated/component.d.ts.map +1 -0
  70. package/dist/component/_generated/component.js +11 -0
  71. package/dist/component/_generated/component.js.map +1 -0
  72. package/dist/component/_generated/dataModel.d.ts +4 -18
  73. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  74. package/dist/component/_generated/dataModel.js +11 -0
  75. package/dist/component/_generated/dataModel.js.map +1 -0
  76. package/dist/component/_generated/server.d.ts +10 -38
  77. package/dist/component/_generated/server.d.ts.map +1 -1
  78. package/dist/component/_generated/server.js +9 -5
  79. package/dist/component/_generated/server.js.map +1 -1
  80. package/dist/component/files.d.ts +16 -10
  81. package/dist/component/files.d.ts.map +1 -1
  82. package/dist/component/files.js +10 -2
  83. package/dist/component/files.js.map +1 -1
  84. package/dist/component/messages.d.ts +2553 -342
  85. package/dist/component/messages.d.ts.map +1 -1
  86. package/dist/component/messages.js +387 -154
  87. package/dist/component/messages.js.map +1 -1
  88. package/dist/component/schema.d.ts +5697 -3584
  89. package/dist/component/schema.d.ts.map +1 -1
  90. package/dist/component/schema.js +18 -41
  91. package/dist/component/schema.js.map +1 -1
  92. package/dist/component/streams.d.ts +35 -335
  93. package/dist/component/streams.d.ts.map +1 -1
  94. package/dist/component/streams.js +114 -73
  95. package/dist/component/streams.js.map +1 -1
  96. package/dist/component/threads.d.ts +16 -16
  97. package/dist/component/users.d.ts +4 -4
  98. package/dist/component/vector/index.d.ts +1 -1
  99. package/dist/component/vector/index.d.ts.map +1 -1
  100. package/dist/component/vector/index.js +1 -3
  101. package/dist/component/vector/index.js.map +1 -1
  102. package/dist/deltas.d.ts +43 -0
  103. package/dist/deltas.d.ts.map +1 -0
  104. package/dist/deltas.js +447 -0
  105. package/dist/deltas.js.map +1 -0
  106. package/dist/mapping.d.ts +20 -20
  107. package/dist/mapping.d.ts.map +1 -1
  108. package/dist/mapping.js +313 -96
  109. package/dist/mapping.js.map +1 -1
  110. package/dist/react/SmoothText.d.ts +5 -0
  111. package/dist/react/SmoothText.d.ts.map +1 -0
  112. package/dist/react/SmoothText.js +6 -0
  113. package/dist/react/SmoothText.js.map +1 -0
  114. package/dist/react/index.d.ts +5 -77
  115. package/dist/react/index.d.ts.map +1 -1
  116. package/dist/react/index.js +6 -160
  117. package/dist/react/index.js.map +1 -1
  118. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  119. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  120. package/dist/react/optimisticallySendMessage.js +35 -9
  121. package/dist/react/optimisticallySendMessage.js.map +1 -1
  122. package/dist/react/types.d.ts +4 -18
  123. package/dist/react/types.d.ts.map +1 -1
  124. package/dist/react/useDeltaStreams.d.ts +10 -0
  125. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  126. package/dist/react/useDeltaStreams.js +101 -0
  127. package/dist/react/useDeltaStreams.js.map +1 -0
  128. package/dist/react/useSmoothText.d.ts +13 -12
  129. package/dist/react/useSmoothText.d.ts.map +1 -1
  130. package/dist/react/useSmoothText.js +32 -15
  131. package/dist/react/useSmoothText.js.map +1 -1
  132. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  133. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  134. package/dist/react/useStreamingUIMessages.js +92 -0
  135. package/dist/react/useStreamingUIMessages.js.map +1 -0
  136. package/dist/react/useThreadMessages.d.ts +104 -0
  137. package/dist/react/useThreadMessages.d.ts.map +1 -0
  138. package/dist/react/useThreadMessages.js +148 -0
  139. package/dist/react/useThreadMessages.js.map +1 -0
  140. package/dist/react/useUIMessages.d.ts +96 -0
  141. package/dist/react/useUIMessages.d.ts.map +1 -0
  142. package/dist/react/useUIMessages.js +108 -0
  143. package/dist/react/useUIMessages.js.map +1 -0
  144. package/dist/shared.d.ts +20 -4
  145. package/dist/shared.d.ts.map +1 -1
  146. package/dist/shared.js +45 -8
  147. package/dist/shared.js.map +1 -1
  148. package/dist/validators.d.ts +22981 -5666
  149. package/dist/validators.d.ts.map +1 -1
  150. package/dist/validators.js +245 -137
  151. package/dist/validators.js.map +1 -1
  152. package/package.json +98 -50
  153. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  154. package/src/UIMessages.test.ts +273 -0
  155. package/src/UIMessages.ts +739 -0
  156. package/src/client/createTool.ts +293 -76
  157. package/src/client/defaultComponent.ts +17 -0
  158. package/src/client/definePlaygroundAPI.ts +67 -31
  159. package/src/client/files.ts +100 -20
  160. package/src/client/index.test.ts +40 -85
  161. package/src/client/index.ts +520 -1290
  162. package/src/client/messages.ts +237 -0
  163. package/src/client/mockModel.ts +245 -0
  164. package/src/client/saveInputMessages.test.ts +583 -0
  165. package/src/client/saveInputMessages.ts +101 -0
  166. package/src/client/search.test.ts +1207 -0
  167. package/src/client/search.ts +577 -70
  168. package/src/client/start.ts +310 -0
  169. package/src/client/streamText.ts +163 -0
  170. package/src/client/streaming.test.ts +186 -0
  171. package/src/client/streaming.ts +219 -97
  172. package/src/client/threads.ts +83 -0
  173. package/src/client/types.ts +368 -219
  174. package/src/client/utils.ts +27 -0
  175. package/src/component/_generated/api.ts +64 -0
  176. package/src/component/_generated/component.ts +4913 -0
  177. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  178. package/src/component/files.ts +11 -2
  179. package/src/component/messages.test.ts +195 -51
  180. package/src/component/messages.ts +490 -201
  181. package/src/component/schema.ts +20 -46
  182. package/src/component/setup.test.ts +7 -0
  183. package/src/component/streams.ts +184 -83
  184. package/src/component/users.test.ts +0 -1
  185. package/src/component/vector/index.ts +1 -3
  186. package/src/deltas.test.ts +626 -0
  187. package/src/deltas.ts +570 -0
  188. package/src/fromUIMessages.test.ts +497 -0
  189. package/src/mapping.test.ts +103 -6
  190. package/src/mapping.ts +422 -161
  191. package/src/react/SmoothText.tsx +9 -0
  192. package/src/react/index.ts +10 -230
  193. package/src/react/optimisticallySendMessage.ts +55 -12
  194. package/src/react/types.ts +6 -39
  195. package/src/react/useDeltaStreams.ts +154 -0
  196. package/src/react/useSmoothText.ts +56 -36
  197. package/src/react/useStreamingUIMessages.ts +143 -0
  198. package/src/react/useThreadMessages.ts +262 -0
  199. package/src/react/useUIMessages.test.ts +255 -0
  200. package/src/react/useUIMessages.ts +195 -0
  201. package/src/shared.ts +88 -12
  202. package/src/test.ts +18 -0
  203. package/src/toUIMessages.test.ts +1269 -0
  204. package/src/validators.test.ts +18 -19
  205. package/src/validators.ts +325 -185
  206. package/dist/client/_generated/_ignore.d.ts +0 -1
  207. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  208. package/dist/client/_generated/_ignore.js +0 -3
  209. package/dist/client/_generated/_ignore.js.map +0 -1
  210. package/dist/client/listMessages.d.ts +0 -22
  211. package/dist/client/listMessages.d.ts.map +0 -1
  212. package/dist/client/listMessages.js +0 -25
  213. package/dist/client/listMessages.js.map +0 -1
  214. package/dist/package.json +0 -3
  215. package/dist/react/deltas.d.ts +0 -26
  216. package/dist/react/deltas.d.ts.map +0 -1
  217. package/dist/react/deltas.js +0 -384
  218. package/dist/react/deltas.js.map +0 -1
  219. package/dist/react/toUIMessages.d.ts +0 -15
  220. package/dist/react/toUIMessages.d.ts.map +0 -1
  221. package/dist/react/toUIMessages.js +0 -211
  222. package/dist/react/toUIMessages.js.map +0 -1
  223. package/src/client/listMessages.ts +0 -38
  224. package/src/component/_generated/api.d.ts +0 -2202
  225. package/src/component/_generated/api.js +0 -23
  226. package/src/component/_generated/server.js +0 -90
  227. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  228. package/src/react/deltas.test.ts +0 -315
  229. package/src/react/deltas.ts +0 -478
  230. package/src/react/toUIMessages.test.ts +0 -420
  231. package/src/react/toUIMessages.ts +0 -253
  232. package/src/vitest.config.ts +0 -7
  233. /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
@@ -1,26 +1,40 @@
1
1
  import {
2
- type ChunkDetector,
3
2
  smoothStream,
3
+ type AsyncIterableStream,
4
+ type ChunkDetector,
4
5
  type StreamTextTransform,
6
+ type TextStreamPart,
5
7
  type ToolSet,
8
+ type UIMessageChunk,
6
9
  } from "ai";
7
- import type {
8
- ProviderOptions,
9
- StreamArgs,
10
- StreamDelta,
11
- StreamMessage,
12
- vTextStreamPartV5,
10
+ import { v } from "convex/values";
11
+ import {
12
+ vMessageDoc,
13
+ vPaginationResult,
14
+ vStreamDelta,
15
+ vStreamMessage,
16
+ type ProviderOptions,
17
+ type StreamArgs,
18
+ type StreamDelta,
19
+ type StreamMessage,
13
20
  } from "../validators.js";
14
- import type { MessageDoc } from "../component/schema.js";
15
21
  import type {
22
+ ActionCtx,
16
23
  AgentComponent,
17
- RunActionCtx,
18
- RunMutationCtx,
19
- RunQueryCtx,
24
+ MutationCtx,
25
+ QueryCtx,
20
26
  SyncStreamsReturnValue,
21
27
  } from "./types.js";
22
- import { omit } from "convex-helpers";
23
- import type { Infer } from "convex/values";
28
+
29
+ export const vStreamMessagesReturnValue = v.object({
30
+ ...vPaginationResult(vMessageDoc).fields,
31
+ streams: v.optional(
32
+ v.union(
33
+ v.object({ kind: v.literal("list"), messages: v.array(vStreamMessage) }),
34
+ v.object({ kind: v.literal("deltas"), deltas: v.array(vStreamDelta) }),
35
+ ),
36
+ ),
37
+ });
24
38
 
25
39
  /**
26
40
  * A function that handles fetching stream deltas, used with the React hooks
@@ -32,42 +46,47 @@ import type { Infer } from "convex/values";
32
46
  * @returns The deltas for each stream from their existing cursor.
33
47
  */
34
48
  export async function syncStreams(
35
- ctx: RunQueryCtx,
49
+ ctx: QueryCtx | MutationCtx | ActionCtx,
36
50
  component: AgentComponent,
37
- args: {
51
+ {
52
+ threadId,
53
+ streamArgs,
54
+ includeStatuses,
55
+ }: {
38
56
  threadId: string;
39
- streamArgs: StreamArgs | undefined;
57
+ streamArgs?: StreamArgs | undefined;
40
58
  // By default, only streaming messages are included.
41
59
  includeStatuses?: ("streaming" | "finished" | "aborted")[];
42
60
  },
43
61
  ): Promise<SyncStreamsReturnValue | undefined> {
44
- if (!args.streamArgs) return undefined;
45
- if (args.streamArgs.kind === "list") {
62
+ if (!streamArgs) return undefined;
63
+ if (streamArgs.kind === "list") {
46
64
  return {
47
65
  kind: "list",
48
66
  messages: await listStreams(ctx, component, {
49
- threadId: args.threadId,
50
- startOrder: args.streamArgs.startOrder,
51
- includeStatuses: args.includeStatuses,
67
+ threadId,
68
+ startOrder: streamArgs.startOrder,
69
+ includeStatuses,
52
70
  }),
53
71
  };
54
72
  } else {
55
73
  return {
56
74
  kind: "deltas",
57
75
  deltas: await ctx.runQuery(component.streams.listDeltas, {
58
- threadId: args.threadId,
59
- cursors: args.streamArgs.cursors,
76
+ threadId,
77
+ cursors: streamArgs.cursors,
60
78
  }),
61
79
  };
62
80
  }
63
81
  }
64
82
 
65
83
  export async function abortStream(
66
- ctx: RunMutationCtx,
84
+ ctx: MutationCtx | ActionCtx,
67
85
  component: AgentComponent,
68
- args: {
69
- reason: string;
70
- } & ({ streamId: string } | { threadId: string; order: number }),
86
+ args: { reason: string } & (
87
+ | { streamId: string }
88
+ | { threadId: string; order: number }
89
+ ),
71
90
  ): Promise<boolean> {
72
91
  if ("streamId" in args) {
73
92
  return await ctx.runMutation(component.streams.abort, {
@@ -93,7 +112,7 @@ export async function abortStream(
93
112
  * @returns The streams for the thread.
94
113
  */
95
114
  export async function listStreams(
96
- ctx: RunQueryCtx,
115
+ ctx: QueryCtx | MutationCtx | ActionCtx,
97
116
  component: AgentComponent,
98
117
  {
99
118
  threadId,
@@ -126,15 +145,31 @@ export type StreamingOptions = {
126
145
  * Defaults to 250.
127
146
  */
128
147
  throttleMs?: number;
148
+ /**
149
+ * If set to true, this will return immediately, as it would if you weren't
150
+ * saving the deltas. Otherwise, the call will "consume" the stream with
151
+ * .consumeStream(), which waits for the stream to finish before returning.
152
+ *
153
+ * When saving deltas, you're often not interactin with the stream otherwise.
154
+ */
155
+ returnImmediately?: boolean;
129
156
  };
130
157
  export const DEFAULT_STREAMING_OPTIONS = {
131
158
  // This chunks by sentences / clauses. Punctuation followed by whitespace.
132
159
  chunking: /[\p{P}\s]/u,
133
160
  throttleMs: 250,
161
+ returnImmediately: false,
134
162
  } satisfies StreamingOptions;
135
163
 
164
+ /**
165
+ *
166
+ * @param options The options passed to `agent.streamText` to decide whether to
167
+ * save deltas while streaming.
168
+ * @param existing The transforms passed to `agent.streamText` to merge with.
169
+ * @returns The merged transforms to pass to the underlying `streamText` call.
170
+ */
136
171
  export function mergeTransforms<TOOLS extends ToolSet>(
137
- options: StreamingOptions | boolean | undefined,
172
+ options: { chunking?: StreamingOptions["chunking"] } | boolean | undefined,
138
173
  existing:
139
174
  | StreamTextTransform<TOOLS>
140
175
  | Array<StreamTextTransform<TOOLS>>
@@ -156,12 +191,21 @@ export function mergeTransforms<TOOLS extends ToolSet>(
156
191
  return transforms;
157
192
  }
158
193
 
159
- export class DeltaStreamer {
160
- public streamId: string | undefined;
161
- public readonly options: Required<StreamingOptions>;
162
- #nextParts: Infer<typeof vTextStreamPartV5>[] = [];
163
- #nextOrder: number;
164
- #nextStepOrder: number;
194
+ /**
195
+ * DeltaStreamer can be used to save a stream of "parts" by writing
196
+ * batches of them in "deltas" to the database so clients can subscribe
197
+ * (using the syncStreams utility and client hooks) and re-hydrate the stream.
198
+ * You can optionally compress the parts, e.g. concatenating text deltas, to
199
+ * optimize the data in transit.
200
+ */
201
+ export class DeltaStreamer<T> {
202
+ streamId: string | undefined;
203
+ public readonly config: {
204
+ throttleMs: number;
205
+ onAsyncAbort: (reason: string) => Promise<void>;
206
+ compress: ((parts: T[]) => T[]) | null;
207
+ };
208
+ #nextParts: T[] = [];
165
209
  #latestWrite: number = 0;
166
210
  #ongoingWrite: Promise<void> | undefined;
167
211
  #cursor: number = 0;
@@ -169,71 +213,94 @@ export class DeltaStreamer {
169
213
 
170
214
  constructor(
171
215
  public readonly component: AgentComponent,
172
- public readonly ctx: RunActionCtx,
173
- options: true | StreamingOptions,
216
+ public readonly ctx: MutationCtx | ActionCtx,
217
+ config: {
218
+ throttleMs: number | undefined;
219
+ onAsyncAbort: (reason: string) => Promise<void>;
220
+ abortSignal: AbortSignal | undefined;
221
+ compress: ((parts: T[]) => T[]) | null;
222
+ },
174
223
  public readonly metadata: {
175
224
  threadId: string;
176
- agentName: string | undefined;
177
- model: string | undefined;
178
- provider: string | undefined;
179
- providerOptions: ProviderOptions | undefined;
180
- userId: string | undefined;
181
- order: number | undefined;
182
- stepOrder: number | undefined;
183
- abortSignal: AbortSignal | undefined;
225
+ userId?: string;
226
+ order: number;
227
+ stepOrder: number;
228
+ agentName?: string;
229
+ model?: string;
230
+ provider?: string;
231
+ providerOptions?: ProviderOptions;
232
+ format: "UIMessageChunk" | "TextStreamPart" | undefined;
184
233
  },
185
234
  ) {
186
- this.options =
187
- typeof options === "boolean"
188
- ? DEFAULT_STREAMING_OPTIONS
189
- : {
190
- ...DEFAULT_STREAMING_OPTIONS,
191
- ...options,
192
- };
235
+ this.config = {
236
+ throttleMs: config.throttleMs ?? DEFAULT_STREAMING_OPTIONS.throttleMs,
237
+ onAsyncAbort: config.onAsyncAbort,
238
+ compress: config.compress,
239
+ };
193
240
  this.#nextParts = [];
194
- this.#nextOrder = metadata.order ?? 0;
195
- this.#nextStepOrder = (metadata.stepOrder ?? 0) + 1;
196
241
  this.abortController = new AbortController();
197
- if (metadata.abortSignal) {
198
- metadata.abortSignal.addEventListener("abort", async () => {
242
+ if (config.abortSignal) {
243
+ config.abortSignal.addEventListener("abort", async () => {
244
+ if (this.abortController.signal.aborted) {
245
+ return;
246
+ }
199
247
  if (this.streamId) {
248
+ this.abortController.abort();
249
+ await this.#ongoingWrite;
200
250
  await this.ctx.runMutation(this.component.streams.abort, {
201
251
  streamId: this.streamId,
202
252
  reason: "abortSignal",
203
253
  });
204
254
  }
205
- this.abortController.abort();
206
255
  });
207
256
  }
208
257
  }
209
- public async addParts(parts: Infer<typeof vTextStreamPartV5>[]) {
258
+
259
+ // Avoid race conditions by only creating once
260
+ #creatingStreamIdPromise: Promise<string> | undefined;
261
+ public async getStreamId() {
262
+ if (this.streamId) {
263
+ return this.streamId;
264
+ }
265
+ if (this.#creatingStreamIdPromise) {
266
+ return this.#creatingStreamIdPromise;
267
+ }
268
+ this.#creatingStreamIdPromise = this.ctx.runMutation(
269
+ this.component.streams.create,
270
+ this.metadata,
271
+ );
272
+ this.streamId = await this.#creatingStreamIdPromise;
273
+ }
274
+
275
+ public async addParts(parts: T[]) {
210
276
  if (this.abortController.signal.aborted) {
211
277
  return;
212
278
  }
213
- if (!this.streamId) {
214
- this.streamId = await this.ctx.runMutation(
215
- this.component.streams.create,
216
- {
217
- ...omit(this.metadata, ["abortSignal"]),
218
- order: this.#nextOrder,
219
- stepOrder: this.#nextStepOrder,
220
- },
221
- );
222
- }
279
+ await this.getStreamId();
223
280
  this.#nextParts.push(...parts);
224
281
  if (
225
282
  !this.#ongoingWrite &&
226
- Date.now() - this.#latestWrite >= this.options.throttleMs
283
+ Date.now() - this.#latestWrite >= this.config.throttleMs
227
284
  ) {
228
285
  this.#ongoingWrite = this.#sendDelta();
229
286
  }
230
287
  }
231
288
 
289
+ public async consumeStream(stream: AsyncIterableStream<T>) {
290
+ for await (const chunk of stream) {
291
+ await this.addParts([chunk]);
292
+ }
293
+ await this.finish();
294
+ }
295
+
232
296
  async #sendDelta() {
233
297
  if (this.abortController.signal.aborted) {
234
298
  return;
235
299
  }
236
300
  const delta = this.#createDelta();
301
+ if (!delta) {
302
+ return;
303
+ }
237
304
  this.#latestWrite = Date.now();
238
305
  try {
239
306
  const success = await this.ctx.runMutation(
@@ -241,16 +308,21 @@ export class DeltaStreamer {
241
308
  delta,
242
309
  );
243
310
  if (!success) {
311
+ await this.config.onAsyncAbort("async abort");
244
312
  this.abortController.abort();
313
+ return;
245
314
  }
246
315
  } catch (e) {
316
+ await this.config.onAsyncAbort(
317
+ e instanceof Error ? e.message : "unknown error",
318
+ );
247
319
  this.abortController.abort();
248
320
  throw e;
249
321
  }
250
322
  // Now that we've sent the delta, check if we need to send another one.
251
323
  if (
252
324
  this.#nextParts.length > 0 &&
253
- Date.now() - this.#latestWrite >= this.options.throttleMs
325
+ Date.now() - this.#latestWrite >= this.config.throttleMs
254
326
  ) {
255
327
  // We send again immediately with the accumulated deltas.
256
328
  this.#ongoingWrite = this.#sendDelta();
@@ -259,47 +331,97 @@ export class DeltaStreamer {
259
331
  }
260
332
  }
261
333
 
262
- #createDelta(): StreamDelta {
334
+ #createDelta(): StreamDelta | undefined {
335
+ if (this.#nextParts.length === 0) {
336
+ return undefined;
337
+ }
263
338
  const start = this.#cursor;
264
339
  const end = start + this.#nextParts.length;
265
340
  this.#cursor = end;
266
- const parts = this.#nextParts;
341
+ const parts = this.config.compress
342
+ ? this.config.compress(this.#nextParts)
343
+ : this.#nextParts;
267
344
  this.#nextParts = [];
268
345
  if (!this.streamId) {
269
346
  throw new Error("Creating a delta before the stream is created");
270
347
  }
271
- return {
348
+ return { streamId: this.streamId, start, end, parts };
349
+ }
350
+
351
+ public async finish() {
352
+ if (!this.streamId) {
353
+ return;
354
+ }
355
+ await this.#ongoingWrite;
356
+ await this.#sendDelta();
357
+ await this.ctx.runMutation(this.component.streams.finish, {
272
358
  streamId: this.streamId,
273
- start,
274
- end,
275
- parts,
276
- };
359
+ });
277
360
  }
278
361
 
279
- public async finish(messages: MessageDoc[]) {
280
- if (this.#ongoingWrite) {
281
- await this.#ongoingWrite;
282
- this.#ongoingWrite = undefined;
362
+ public async fail(reason: string) {
363
+ if (this.abortController.signal.aborted) {
364
+ return;
283
365
  }
366
+ this.abortController.abort();
284
367
  if (!this.streamId) {
285
- throw new Error("Finish called before stream is created");
368
+ return;
369
+ }
370
+ await this.#ongoingWrite;
371
+ await this.ctx.runMutation(this.component.streams.abort, {
372
+ streamId: this.streamId,
373
+ reason,
374
+ });
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Compressing parts when streaming to save bandwidth in deltas.
380
+ */
381
+
382
+ export function compressUIMessageChunks(
383
+ parts: UIMessageChunk[],
384
+ ): UIMessageChunk[] {
385
+ const compressed: UIMessageChunk[] = [];
386
+ for (const part of parts) {
387
+ const last = compressed.at(-1);
388
+ if (part.type === "text-delta" || part.type === "reasoning-delta") {
389
+ if (last?.type === part.type && part.id === last.id) {
390
+ last.delta += part.delta;
391
+ } else {
392
+ compressed.push(part);
393
+ }
394
+ } else {
395
+ compressed.push(part);
286
396
  }
287
- const lastMessage = messages.at(-1);
288
- if (lastMessage) {
289
- this.#nextOrder = lastMessage.order;
290
- this.#nextStepOrder = lastMessage.stepOrder + 1;
397
+ }
398
+ return compressed;
399
+ }
400
+
401
+ export function compressTextStreamParts(
402
+ parts: TextStreamPart<ToolSet>[],
403
+ ): TextStreamPart<ToolSet>[] {
404
+ const compressed: TextStreamPart<ToolSet>[] = [];
405
+ for (const part of parts) {
406
+ const last = compressed.at(-1);
407
+ if (part.type === "text-delta" || part.type === "reasoning-delta") {
408
+ if (last?.type === part.type && part.id === last.id) {
409
+ last.text += part.text;
410
+ } else {
411
+ compressed.push(part);
412
+ }
291
413
  } else {
292
- console.warn("Step finished without generating a message");
414
+ if (part.type === "file") {
415
+ compressed.push({
416
+ type: "file",
417
+ file: {
418
+ ...part.file,
419
+ uint8Array: undefined as unknown as Uint8Array,
420
+ },
421
+ });
422
+ }
423
+ compressed.push(part);
293
424
  }
294
- const finalDelta =
295
- this.#nextParts.length > 0 ? this.#createDelta() : undefined;
296
- this.#nextParts = [];
297
- const streamId = this.streamId;
298
- this.streamId = undefined;
299
- this.#cursor = 0;
300
- await this.ctx.runMutation(this.component.streams.finish, {
301
- streamId,
302
- finalDelta,
303
- });
304
425
  }
426
+ return compressed;
305
427
  }
@@ -0,0 +1,83 @@
1
+ import type { WithoutSystemFields } from "convex/server";
2
+ import type { ThreadDoc } from "../validators.js";
3
+ import type {
4
+ ActionCtx,
5
+ AgentComponent,
6
+ MutationCtx,
7
+ QueryCtx,
8
+ } from "./types.js";
9
+
10
+ /**
11
+ * Create a thread to store messages with an Agent.
12
+ * @param ctx The context from a mutation or action.
13
+ * @param component The Agent component, usually `components.agent`.
14
+ * @param args The associated thread metadata.
15
+ * @returns The id of the created thread.
16
+ */
17
+ export async function createThread(
18
+ ctx: MutationCtx | ActionCtx,
19
+ component: AgentComponent,
20
+ args?: { userId?: string | null; title?: string; summary?: string },
21
+ ) {
22
+ const { _id: threadId } = await ctx.runMutation(
23
+ component.threads.createThread,
24
+ {
25
+ userId: args?.userId ?? undefined,
26
+ title: args?.title,
27
+ summary: args?.summary,
28
+ },
29
+ );
30
+ return threadId;
31
+ }
32
+
33
+ /**
34
+ * Get the metadata for a thread.
35
+ * @param ctx A ctx object from a query, mutation, or action.
36
+ * @param args.threadId The thread to get the metadata for.
37
+ * @returns The metadata for the thread.
38
+ */
39
+ export async function getThreadMetadata(
40
+ ctx: QueryCtx | MutationCtx | ActionCtx,
41
+ component: AgentComponent,
42
+ args: { threadId: string },
43
+ ): Promise<ThreadDoc> {
44
+ const thread = await ctx.runQuery(component.threads.getThread, {
45
+ threadId: args.threadId,
46
+ });
47
+ if (!thread) {
48
+ throw new Error("Thread not found");
49
+ }
50
+ return thread;
51
+ }
52
+
53
+ export async function updateThreadMetadata(
54
+ ctx: MutationCtx | ActionCtx,
55
+ component: AgentComponent,
56
+ args: { threadId: string; patch: Partial<WithoutSystemFields<ThreadDoc>> },
57
+ ) {
58
+ return ctx.runMutation(component.threads.updateThread, {
59
+ threadId: args.threadId,
60
+ patch: args.patch,
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Search for threads by title, paginated.
66
+ * @param ctx The context passed from the query/mutation/action.
67
+ * @returns The threads matching the search, paginated.
68
+ */
69
+ export async function searchThreadTitles(
70
+ ctx: QueryCtx | MutationCtx | ActionCtx,
71
+ component: AgentComponent,
72
+ {
73
+ userId,
74
+ query,
75
+ limit,
76
+ }: { userId?: string | undefined; query: string; limit?: number },
77
+ ): Promise<ThreadDoc[]> {
78
+ return ctx.runQuery(component.threads.searchThreadTitles, {
79
+ userId,
80
+ query,
81
+ limit: limit ?? 10,
82
+ });
83
+ }