@convex-dev/agent 0.2.5-alpha.0 → 0.2.6-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 (55) hide show
  1. package/dist/client/definePlaygroundAPI.d.ts +248 -18
  2. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  3. package/dist/client/index.d.ts +211 -33
  4. package/dist/client/index.d.ts.map +1 -1
  5. package/dist/client/index.js +56 -52
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/client/messages.d.ts +50 -4
  8. package/dist/client/messages.d.ts.map +1 -1
  9. package/dist/client/mockModel.d.ts +30 -0
  10. package/dist/client/mockModel.d.ts.map +1 -0
  11. package/dist/client/mockModel.js +153 -0
  12. package/dist/client/mockModel.js.map +1 -0
  13. package/dist/client/search.d.ts +50 -4
  14. package/dist/client/search.d.ts.map +1 -1
  15. package/dist/client/streaming.d.ts +603 -46
  16. package/dist/client/streaming.d.ts.map +1 -1
  17. package/dist/client/streaming.js +30 -35
  18. package/dist/client/streaming.js.map +1 -1
  19. package/dist/client/textStreamParts.d.ts +5 -0
  20. package/dist/client/textStreamParts.d.ts.map +1 -0
  21. package/dist/{parts.js → client/textStreamParts.js} +18 -1
  22. package/dist/client/textStreamParts.js.map +1 -0
  23. package/dist/component/_generated/api.d.ts +240 -16
  24. package/dist/component/messages.d.ts +549 -43
  25. package/dist/component/messages.d.ts.map +1 -1
  26. package/dist/component/messages.js +1 -1
  27. package/dist/component/messages.js.map +1 -1
  28. package/dist/component/schema.d.ts +1546 -98
  29. package/dist/component/schema.d.ts.map +1 -1
  30. package/dist/mapping.d.ts.map +1 -1
  31. package/dist/mapping.js +30 -18
  32. package/dist/mapping.js.map +1 -1
  33. package/dist/react/useSmoothText.d.ts +1 -1
  34. package/dist/react/useSmoothText.d.ts.map +1 -1
  35. package/dist/react/useSmoothText.js +30 -22
  36. package/dist/react/useSmoothText.js.map +1 -1
  37. package/dist/validators.d.ts +3740 -201
  38. package/dist/validators.d.ts.map +1 -1
  39. package/dist/validators.js +10 -1
  40. package/dist/validators.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/client/index.ts +90 -82
  43. package/src/client/mockModel.ts +195 -0
  44. package/src/client/streaming.ts +46 -54
  45. package/src/{parts.ts → client/textStreamParts.ts} +33 -1
  46. package/src/component/_generated/api.d.ts +240 -16
  47. package/src/component/messages.ts +1 -1
  48. package/src/mapping.test.ts +50 -1
  49. package/src/mapping.ts +42 -17
  50. package/src/react/toUIMessages.test.ts +16 -4
  51. package/src/react/useSmoothText.ts +40 -31
  52. package/src/validators.ts +23 -1
  53. package/dist/parts.d.ts +0 -3
  54. package/dist/parts.d.ts.map +0 -1
  55. package/dist/parts.js.map +0 -1
@@ -0,0 +1,195 @@
1
+ import type {
2
+ LanguageModelV2,
3
+ LanguageModelV2StreamPart,
4
+ } from "@ai-sdk/provider";
5
+ import type { ReasoningPart, TextPart } from "@ai-sdk/provider-utils";
6
+ import { simulateReadableStream } from "ai";
7
+
8
+ const longDefaultText = `
9
+ A A A A A A A A A A A A A A A
10
+ B B B B B B B B B B B B B B B
11
+ C C C C C C C C C C C C C C C
12
+ D D D D D D D D D D D D D D D
13
+ `;
14
+ const defaultUsage = { outputTokens: 10, inputTokens: 3, totalTokens: 13 };
15
+
16
+ export type MockModelArgs = {
17
+ provider?: LanguageModelV2["provider"];
18
+ modelId?: LanguageModelV2["modelId"];
19
+ supportedUrls?:
20
+ | LanguageModelV2["supportedUrls"]
21
+ | (() => LanguageModelV2["supportedUrls"]);
22
+ chunkDelayInMs?: number;
23
+ initialDelayInMs?: number;
24
+ // provide either content or doGenerate & doStream
25
+ content?: (TextPart | ReasoningPart)[];
26
+ doGenerate?: LanguageModelV2["doGenerate"];
27
+ doStream?: LanguageModelV2["doStream"];
28
+ fail?:
29
+ | boolean
30
+ | {
31
+ probability?: number;
32
+ error?: string;
33
+ };
34
+ };
35
+
36
+ export function mockModel(args?: MockModelArgs): LanguageModelV2 {
37
+ return new MockLanguageModel(args ?? {});
38
+ }
39
+
40
+ export class MockLanguageModel implements LanguageModelV2 {
41
+ readonly specificationVersion = "v2";
42
+
43
+ private _supportedUrls: () => LanguageModelV2["supportedUrls"];
44
+
45
+ readonly provider: LanguageModelV2["provider"];
46
+ readonly modelId: LanguageModelV2["modelId"];
47
+
48
+ doGenerate: LanguageModelV2["doGenerate"];
49
+ doStream: LanguageModelV2["doStream"];
50
+
51
+ doGenerateCalls: Parameters<LanguageModelV2["doGenerate"]>[0][] = [];
52
+ doStreamCalls: Parameters<LanguageModelV2["doStream"]>[0][] = [];
53
+
54
+ constructor(args: MockModelArgs) {
55
+ this.provider = args.provider || "mock-provider";
56
+ this.modelId = args.modelId || "mock-model-id";
57
+ const {
58
+ content = [{ type: "text", text: longDefaultText }],
59
+ chunkDelayInMs = 200,
60
+ initialDelayInMs = 1000,
61
+ supportedUrls = {},
62
+ } = args;
63
+ const fail =
64
+ args.fail &&
65
+ (args.fail === true ||
66
+ !args.fail.probability ||
67
+ Math.random() < args.fail.probability);
68
+ const error =
69
+ (typeof args.fail === "object" && args.fail.error) ||
70
+ "Mock error message";
71
+
72
+ const chunks: LanguageModelV2StreamPart[] = [
73
+ { type: "stream-start", warnings: [] },
74
+ ];
75
+ chunks.push(
76
+ ...content.flatMap((c, ci) => {
77
+ const deltas = c.text.split(" ");
78
+ let parts: LanguageModelV2StreamPart[] = [];
79
+ if (c.type === "reasoning") {
80
+ parts.push({
81
+ type: "reasoning-start",
82
+ id: `${ci}-reasoning-start`,
83
+ });
84
+ parts.push(
85
+ ...deltas.map(
86
+ (delta, di) =>
87
+ ({
88
+ type: "reasoning-delta",
89
+ delta,
90
+ id: `${ci}-reasoning-${di}`,
91
+ providerMetadata: {
92
+ mockProvider: { mock: { reasoningDetails: null } },
93
+ },
94
+ }) satisfies LanguageModelV2StreamPart,
95
+ ),
96
+ );
97
+ parts.push({
98
+ type: "reasoning-end",
99
+ id: `${ci}-reasoning-end`,
100
+ });
101
+ } else if (c.type === "text") {
102
+ parts.push({
103
+ type: "text-start",
104
+ id: `${ci}-text-start`,
105
+ });
106
+ parts = deltas.map((delta, di) => ({
107
+ type: "text-delta",
108
+ delta,
109
+ id: `${ci}-text-${di}`,
110
+ }));
111
+ parts.push({
112
+ type: "text-end",
113
+ id: `${ci}-text-end`,
114
+ });
115
+ }
116
+ return parts;
117
+ }),
118
+ );
119
+ if (fail) {
120
+ chunks.push({
121
+ type: "error",
122
+ error,
123
+ });
124
+ }
125
+ chunks.push({
126
+ type: "finish",
127
+ finishReason: fail ? "error" : "stop",
128
+ usage: defaultUsage,
129
+ providerMetadata: {
130
+ mockProvider: { mock: "mock metadata" },
131
+ },
132
+ });
133
+ this.doGenerate = async (options) => {
134
+ this.doGenerateCalls.push(options);
135
+
136
+ if (fail) {
137
+ throw new Error(error);
138
+ }
139
+ if (typeof args.doGenerate === "function") {
140
+ return args.doGenerate(options);
141
+ } else if (Array.isArray(args.doGenerate)) {
142
+ return args.doGenerate[this.doGenerateCalls.length];
143
+ } else if (content) {
144
+ return {
145
+ content,
146
+ finishReason: "stop",
147
+ usage: defaultUsage,
148
+ providerMetadata: { mockProvider: { mock: "mock metadata" } },
149
+ warnings: [],
150
+ };
151
+ } else {
152
+ throw new Error("Unexpected: no content or doGenerate");
153
+ }
154
+ };
155
+ this._supportedUrls =
156
+ typeof supportedUrls === "function"
157
+ ? supportedUrls
158
+ : async () => supportedUrls;
159
+ this.doStream = async (options) => {
160
+ this.doStreamCalls.push(options);
161
+
162
+ if (typeof args.doStream === "function") {
163
+ return args.doStream(options);
164
+ } else if (Array.isArray(args.doStream)) {
165
+ return args.doStream[this.doStreamCalls.length];
166
+ } else if (content) {
167
+ const stream = simulateReadableStream({
168
+ chunks,
169
+ initialDelayInMs,
170
+ chunkDelayInMs,
171
+ });
172
+ if (options.abortSignal) {
173
+ throw new Error("abortSignal in mock model");
174
+ }
175
+ return {
176
+ stream,
177
+ request: { body: {} },
178
+ response: { headers: {} },
179
+ };
180
+ } else if (args.doStream) {
181
+ return args.doStream;
182
+ } else {
183
+ throw new Error("Provide either content or doStream");
184
+ }
185
+ };
186
+ this._supportedUrls =
187
+ typeof supportedUrls === "function"
188
+ ? supportedUrls
189
+ : async () => supportedUrls;
190
+ }
191
+
192
+ get supportedUrls() {
193
+ return this._supportedUrls();
194
+ }
195
+ }
@@ -1,10 +1,4 @@
1
- import {
2
- type ChunkDetector,
3
- smoothStream,
4
- type StreamTextTransform,
5
- type TextStreamPart,
6
- type ToolSet,
7
- } from "ai";
1
+ import { type ChunkDetector } from "ai";
8
2
  import {
9
3
  vStreamDelta,
10
4
  vStreamMessage,
@@ -21,8 +15,6 @@ import type {
21
15
  RunQueryCtx,
22
16
  SyncStreamsReturnValue,
23
17
  } from "./types.js";
24
- import { omit } from "convex-helpers";
25
- import { serializeTextStreamingPartsV5 } from "../parts.js";
26
18
  import { v } from "convex/values";
27
19
  import { vMessageDoc } from "../component/schema.js";
28
20
 
@@ -161,33 +153,21 @@ export const DEFAULT_STREAMING_OPTIONS = {
161
153
  returnImmediately: false,
162
154
  } satisfies StreamingOptions;
163
155
 
164
- export function mergeTransforms<TOOLS extends ToolSet>(
165
- options: StreamingOptions | boolean | undefined,
166
- existing:
167
- | StreamTextTransform<TOOLS>
168
- | Array<StreamTextTransform<TOOLS>>
169
- | undefined,
170
- ) {
171
- if (!options) {
172
- return existing;
173
- }
174
- const chunking =
175
- typeof options === "boolean"
176
- ? DEFAULT_STREAMING_OPTIONS.chunking
177
- : options.chunking;
178
- const transforms = Array.isArray(existing)
179
- ? existing
180
- : existing
181
- ? [existing]
182
- : [];
183
- transforms.push(smoothStream({ delayInMs: null, chunking }));
184
- return transforms;
185
- }
186
-
187
- export class DeltaStreamer {
156
+ /**
157
+ * DeltaStreamer can be used to save a stream of "parts" by writing
158
+ * batches of them in "deltas" to the database so clients can subscribe
159
+ * (using the syncStreams utility and client hooks) and re-hydrate the stream.
160
+ * You can optionally compress the parts, e.g. concatenating text deltas, to
161
+ * optimize the data in transit.
162
+ */
163
+ export class DeltaStreamer<T> {
188
164
  public streamId: string | undefined;
189
- public readonly options: Required<StreamingOptions>;
190
- #nextParts: TextStreamPart<ToolSet>[] = [];
165
+ public readonly config: {
166
+ stream: Required<StreamingOptions>;
167
+ onAsyncAbort: (reason: string) => Promise<void>;
168
+ compress: ((parts: T[]) => T[]) | null;
169
+ };
170
+ #nextParts: T[] = [];
191
171
  #latestWrite: number = 0;
192
172
  #ongoingWrite: Promise<void> | undefined;
193
173
  #cursor: number = 0;
@@ -196,7 +176,12 @@ export class DeltaStreamer {
196
176
  constructor(
197
177
  public readonly component: AgentComponent,
198
178
  public readonly ctx: RunActionCtx,
199
- options: true | StreamingOptions,
179
+ config: {
180
+ stream: true | StreamingOptions;
181
+ onAsyncAbort: (reason: string) => Promise<void>;
182
+ abortSignal: AbortSignal | undefined;
183
+ compress: ((parts: T[]) => T[]) | null;
184
+ },
200
185
  public readonly metadata: {
201
186
  threadId: string;
202
187
  userId?: string;
@@ -206,45 +191,49 @@ export class DeltaStreamer {
206
191
  model?: string;
207
192
  provider?: string;
208
193
  providerOptions?: ProviderOptions;
209
- abortSignal?: AbortSignal;
210
194
  },
211
195
  ) {
212
- this.options =
213
- typeof options === "boolean"
214
- ? DEFAULT_STREAMING_OPTIONS
215
- : { ...DEFAULT_STREAMING_OPTIONS, ...options };
196
+ this.config = {
197
+ stream:
198
+ config.stream === true
199
+ ? DEFAULT_STREAMING_OPTIONS
200
+ : { ...DEFAULT_STREAMING_OPTIONS, ...config },
201
+ onAsyncAbort: config.onAsyncAbort,
202
+ compress: config.compress,
203
+ };
216
204
  this.#nextParts = [];
217
205
  this.abortController = new AbortController();
218
- if (metadata.abortSignal) {
219
- metadata.abortSignal.addEventListener("abort", async () => {
206
+ if (config.abortSignal) {
207
+ config.abortSignal.addEventListener("abort", async () => {
208
+ if (this.abortController.signal.aborted) {
209
+ return;
210
+ }
220
211
  if (this.streamId) {
221
212
  this.abortController.abort();
222
- const finalDelta = this.#createDelta();
223
213
  await this.#ongoingWrite;
224
214
  await this.ctx.runMutation(this.component.streams.abort, {
225
215
  streamId: this.streamId,
226
216
  reason: "abortSignal",
227
- finalDelta,
228
217
  });
229
218
  }
230
219
  });
231
220
  }
232
221
  }
233
222
 
234
- public async addParts(parts: TextStreamPart<ToolSet>[]) {
223
+ public async addParts(parts: T[]) {
235
224
  if (this.abortController.signal.aborted) {
236
225
  return;
237
226
  }
238
227
  if (!this.streamId) {
239
228
  this.streamId = await this.ctx.runMutation(
240
229
  this.component.streams.create,
241
- omit(this.metadata, ["abortSignal"]),
230
+ this.metadata,
242
231
  );
243
232
  }
244
233
  this.#nextParts.push(...parts);
245
234
  if (
246
235
  !this.#ongoingWrite &&
247
- Date.now() - this.#latestWrite >= this.options.throttleMs
236
+ Date.now() - this.#latestWrite >= this.config.stream.throttleMs
248
237
  ) {
249
238
  this.#ongoingWrite = this.#sendDelta();
250
239
  }
@@ -265,16 +254,21 @@ export class DeltaStreamer {
265
254
  delta,
266
255
  );
267
256
  if (!success) {
257
+ await this.config.onAsyncAbort("async abort");
268
258
  this.abortController.abort();
259
+ return;
269
260
  }
270
261
  } catch (e) {
262
+ await this.config.onAsyncAbort(
263
+ e instanceof Error ? e.message : "unknown error",
264
+ );
271
265
  this.abortController.abort();
272
266
  throw e;
273
267
  }
274
268
  // Now that we've sent the delta, check if we need to send another one.
275
269
  if (
276
270
  this.#nextParts.length > 0 &&
277
- Date.now() - this.#latestWrite >= this.options.throttleMs
271
+ Date.now() - this.#latestWrite >= this.config.stream.throttleMs
278
272
  ) {
279
273
  // We send again immediately with the accumulated deltas.
280
274
  this.#ongoingWrite = this.#sendDelta();
@@ -290,7 +284,9 @@ export class DeltaStreamer {
290
284
  const start = this.#cursor;
291
285
  const end = start + this.#nextParts.length;
292
286
  this.#cursor = end;
293
- const parts = serializeTextStreamingPartsV5(this.#nextParts);
287
+ const parts = this.config.compress
288
+ ? this.config.compress(this.#nextParts)
289
+ : this.#nextParts;
294
290
  this.#nextParts = [];
295
291
  if (!this.streamId) {
296
292
  throw new Error("Creating a delta before the stream is created");
@@ -302,11 +298,9 @@ export class DeltaStreamer {
302
298
  if (!this.streamId) {
303
299
  return;
304
300
  }
305
- const finalDelta = this.#createDelta();
306
301
  await this.#ongoingWrite;
307
302
  await this.ctx.runMutation(this.component.streams.finish, {
308
303
  streamId: this.streamId,
309
- finalDelta,
310
304
  });
311
305
  }
312
306
 
@@ -318,12 +312,10 @@ export class DeltaStreamer {
318
312
  if (!this.streamId) {
319
313
  return;
320
314
  }
321
- const finalDelta = this.#createDelta();
322
315
  await this.#ongoingWrite;
323
316
  await this.ctx.runMutation(this.component.streams.abort, {
324
317
  streamId: this.streamId,
325
318
  reason,
326
- finalDelta,
327
319
  });
328
320
  }
329
321
  }
@@ -1,4 +1,13 @@
1
- import type { TextStreamPart, ToolSet } from "ai";
1
+ import {
2
+ smoothStream,
3
+ type StreamTextTransform,
4
+ type ToolSet,
5
+ type TextStreamPart,
6
+ } from "ai";
7
+ import {
8
+ DEFAULT_STREAMING_OPTIONS,
9
+ type StreamingOptions,
10
+ } from "./streaming.js";
2
11
 
3
12
  export function serializeTextStreamingPartsV5(
4
13
  parts: TextStreamPart<ToolSet>[],
@@ -37,3 +46,26 @@ export function serializeTextStreamingPartsV5(
37
46
  }
38
47
  return compressed;
39
48
  }
49
+
50
+ export function mergeTransforms<TOOLS extends ToolSet>(
51
+ options: StreamingOptions | boolean | undefined,
52
+ existing:
53
+ | StreamTextTransform<TOOLS>
54
+ | Array<StreamTextTransform<TOOLS>>
55
+ | undefined,
56
+ ) {
57
+ if (!options) {
58
+ return existing;
59
+ }
60
+ const chunking =
61
+ typeof options === "boolean"
62
+ ? DEFAULT_STREAMING_OPTIONS.chunking
63
+ : options.chunking;
64
+ const transforms = Array.isArray(existing)
65
+ ? existing
66
+ : existing
67
+ ? [existing]
68
+ : [];
69
+ transforms.push(smoothStream({ delayInMs: null, chunking }));
70
+ return transforms;
71
+ }