@convex-dev/agent 0.7.1 → 0.7.3

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 (90) hide show
  1. package/README.md +5 -8
  2. package/dist/client/messages.d.ts +10 -0
  3. package/dist/client/messages.d.ts.map +1 -1
  4. package/dist/client/messages.js +1 -0
  5. package/dist/client/messages.js.map +1 -1
  6. package/dist/component/_generated/component.d.ts +10 -8
  7. package/dist/component/_generated/component.d.ts.map +1 -1
  8. package/dist/component/messages.d.ts +340 -338
  9. package/dist/component/messages.d.ts.map +1 -1
  10. package/dist/component/messages.js +74 -14
  11. package/dist/component/messages.js.map +1 -1
  12. package/dist/component/schema.d.ts +726 -622
  13. package/dist/component/schema.d.ts.map +1 -1
  14. package/dist/component/streams.d.ts +6 -1
  15. package/dist/component/streams.d.ts.map +1 -1
  16. package/dist/component/streams.js +14 -15
  17. package/dist/component/streams.js.map +1 -1
  18. package/dist/component/threads.d.ts.map +1 -1
  19. package/dist/component/threads.js +1 -0
  20. package/dist/component/threads.js.map +1 -1
  21. package/dist/component/vector/index.d.ts +9 -9
  22. package/dist/component/vector/index.d.ts.map +1 -1
  23. package/dist/component/vector/tables.d.ts +5 -5
  24. package/dist/component/vector/tables.d.ts.map +1 -1
  25. package/dist/component/vector/tables.js +1 -1
  26. package/dist/component/vector/tables.js.map +1 -1
  27. package/dist/errors.d.ts +2 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +106 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/validators.d.ts +1952 -1952
  32. package/dist/vercel/client/definePlaygroundAPI.d.ts +132 -132
  33. package/dist/vercel/client/messages.d.ts +50 -32
  34. package/dist/vercel/client/messages.d.ts.map +1 -1
  35. package/dist/vercel/client/messages.js +2 -0
  36. package/dist/vercel/client/messages.js.map +1 -1
  37. package/dist/vercel/client/search.d.ts +475 -32
  38. package/dist/vercel/client/search.d.ts.map +1 -1
  39. package/dist/vercel/client/search.js +34 -9
  40. package/dist/vercel/client/search.js.map +1 -1
  41. package/dist/vercel/client/start.d.ts.map +1 -1
  42. package/dist/vercel/client/start.js +1 -0
  43. package/dist/vercel/client/start.js.map +1 -1
  44. package/dist/vercel/client/streamText.d.ts +1 -2
  45. package/dist/vercel/client/streamText.d.ts.map +1 -1
  46. package/dist/vercel/client/streamText.js +79 -22
  47. package/dist/vercel/client/streamText.js.map +1 -1
  48. package/dist/vercel/client/streaming.d.ts +326 -305
  49. package/dist/vercel/client/streaming.d.ts.map +1 -1
  50. package/dist/vercel/client/streaming.js +140 -25
  51. package/dist/vercel/client/streaming.js.map +1 -1
  52. package/dist/vercel/client/types.d.ts +2 -0
  53. package/dist/vercel/client/types.d.ts.map +1 -1
  54. package/dist/vercel/client/types.js.map +1 -1
  55. package/dist/vercel/client/utils.d.ts +9 -5
  56. package/dist/vercel/client/utils.d.ts.map +1 -1
  57. package/dist/vercel/client/utils.js +13 -13
  58. package/dist/vercel/client/utils.js.map +1 -1
  59. package/dist/vercel/index.d.ts +91 -90
  60. package/dist/vercel/index.d.ts.map +1 -1
  61. package/dist/vercel/index.js +2 -0
  62. package/dist/vercel/index.js.map +1 -1
  63. package/package.json +16 -9
  64. package/src/client/messages.ts +12 -0
  65. package/src/component/_generated/component.ts +10 -0
  66. package/src/component/messages.test.ts +404 -0
  67. package/src/component/messages.ts +94 -12
  68. package/src/component/streams.test.ts +54 -1
  69. package/src/component/streams.ts +20 -17
  70. package/src/component/threads.test.ts +39 -0
  71. package/src/component/threads.ts +1 -0
  72. package/src/component/vector/tables.ts +1 -1
  73. package/src/errors.test.ts +76 -0
  74. package/src/errors.ts +113 -0
  75. package/src/vercel/client/deltaFlush.test.ts +106 -0
  76. package/src/vercel/client/index.test.ts +47 -0
  77. package/src/vercel/client/messages.ts +20 -0
  78. package/src/vercel/client/search.test.ts +183 -8
  79. package/src/vercel/client/search.ts +46 -12
  80. package/src/vercel/client/start.ts +1 -0
  81. package/src/vercel/client/streamText.test.ts +577 -18
  82. package/src/vercel/client/streamText.ts +84 -32
  83. package/src/vercel/client/streaming.integration.test.ts +40 -3
  84. package/src/vercel/client/streaming.test.ts +101 -7
  85. package/src/vercel/client/streaming.throttle.test.ts +144 -0
  86. package/src/vercel/client/streaming.ts +161 -25
  87. package/src/vercel/client/types.ts +2 -0
  88. package/src/vercel/client/utils.test.ts +97 -0
  89. package/src/vercel/client/utils.ts +15 -15
  90. package/src/vercel/index.ts +2 -0
@@ -8,6 +8,7 @@ import {
8
8
  type UIMessageChunk,
9
9
  } from "ai";
10
10
  import { v } from "convex/values";
11
+ import { errorToString } from "../../errors.js";
11
12
  import {
12
13
  vMessageDoc,
13
14
  vPaginationResult,
@@ -132,6 +133,11 @@ export async function listStreams(
132
133
  }
133
134
 
134
135
  export type StreamingOptions = {
136
+ /**
137
+ * Whether source parts emitted by the model are included in the persisted
138
+ * delta stream. Defaults to false.
139
+ */
140
+ sendSources?: boolean;
135
141
  /**
136
142
  * The minimum granularity of deltas to save.
137
143
  * Note: this is not a guarantee that every delta will be exactly one line.
@@ -155,6 +161,7 @@ export type StreamingOptions = {
155
161
  returnImmediately?: boolean;
156
162
  };
157
163
  export const DEFAULT_STREAMING_OPTIONS = {
164
+ sendSources: false,
158
165
  // This chunks by sentences / clauses. Punctuation followed by whitespace.
159
166
  chunking: /[\p{P}\s]/u,
160
167
  throttleMs: 250,
@@ -214,12 +221,22 @@ export class DeltaStreamer<T> {
214
221
  #nextParts: T[] = [];
215
222
  #latestWrite: number = 0;
216
223
  #ongoingWrite: Promise<void> | undefined;
224
+ #flushTimer: ReturnType<typeof setTimeout> | undefined;
217
225
  #abortPromise: Promise<void> | undefined;
218
226
  #cursor: number = 0;
219
227
  public abortController: AbortController;
220
- // When true, the stream will be finished externally (e.g., atomically via addMessages)
221
- // and consumeStream should skip calling finish().
222
- #finishedExternally: boolean = false;
228
+ /**
229
+ * When true, external code finishes the stream row (atomically with the
230
+ * message save, issue #181) and `consumeStream` must not. Decided once, at
231
+ * construction.
232
+ */
233
+ #finishHandledExternally: boolean;
234
+ /**
235
+ * Set only where the row is finished before the source is drained. An
236
+ * optimization, not a race guard: `addDelta` already returns false for a
237
+ * non-streaming row.
238
+ */
239
+ #stoppedAccepting: boolean = false;
223
240
 
224
241
  constructor(
225
242
  public readonly component: AgentComponent,
@@ -229,6 +246,8 @@ export class DeltaStreamer<T> {
229
246
  onAsyncAbort: (reason: string) => Promise<void>;
230
247
  abortSignal: AbortSignal | undefined;
231
248
  compress: ((parts: T[]) => T[]) | null;
249
+ /** Defaults to false, meaning `consumeStream` finishes the stream. */
250
+ finishHandledExternally?: boolean;
232
251
  materialize?: (parts: T[]) => Promise<{
233
252
  parts: T[];
234
253
  fileRefs: Array<{ url: string; fileId: string }>;
@@ -252,6 +271,7 @@ export class DeltaStreamer<T> {
252
271
  compress: config.compress,
253
272
  materialize: config.materialize ?? null,
254
273
  };
274
+ this.#finishHandledExternally = config.finishHandledExternally ?? false;
255
275
  this.#nextParts = [];
256
276
  this.abortController = new AbortController();
257
277
  if (config.abortSignal) {
@@ -293,20 +313,64 @@ export class DeltaStreamer<T> {
293
313
  if (this.abortController.signal.aborted) {
294
314
  return;
295
315
  }
296
- // Once the stream has been finished externally (e.g. by the inline
297
- // save in streamText's onStepFinish for the returnImmediately path),
298
- // the stream record is already "finished" in the DB. Late deltas
299
- // would be silently dropped by streams.addDelta — skip the work.
300
- if (this.#finishedExternally) {
316
+ if (this.#stoppedAccepting) {
301
317
  return;
302
318
  }
303
- await this.getStreamId();
319
+ // Buffer before awaiting: a part parked in stream creation would be
320
+ // invisible to #flushPendingParts.
304
321
  this.#nextParts.push(...parts);
322
+ const streamId = await this.getOrCreateStreamId({
323
+ ifAborted: "returnUndefined",
324
+ });
325
+ if (!streamId) return;
326
+ if (this.#stoppedAccepting || this.abortController.signal.aborted) {
327
+ return;
328
+ }
305
329
  if (
306
330
  !this.#ongoingWrite &&
307
331
  Date.now() - this.#latestWrite >= this.config.throttleMs
308
332
  ) {
309
333
  this.#ongoingWrite = this.#sendDelta();
334
+ } else {
335
+ this.#scheduleFlush();
336
+ }
337
+ }
338
+
339
+ // The throttle is only reconsidered when the next part arrives, so a pause in
340
+ // the stream would otherwise hold whatever is buffered until it resumes.
341
+ #scheduleFlush() {
342
+ if (this.#flushTimer) {
343
+ return;
344
+ }
345
+ const wait = Math.max(
346
+ 0,
347
+ this.config.throttleMs - (Date.now() - this.#latestWrite),
348
+ );
349
+ this.#flushTimer = setTimeout(() => {
350
+ this.#flushTimer = undefined;
351
+ if (
352
+ this.#ongoingWrite ||
353
+ this.#nextParts.length === 0 ||
354
+ this.#stoppedAccepting ||
355
+ this.abortController.signal.aborted
356
+ ) {
357
+ return;
358
+ }
359
+ // A write can start and land while this wake is armed, which moves the
360
+ // deadline out from under it. Re-arm against the current one rather
361
+ // than publishing early.
362
+ if (Date.now() - this.#latestWrite < this.config.throttleMs) {
363
+ this.#scheduleFlush();
364
+ return;
365
+ }
366
+ this.#ongoingWrite = this.#sendDelta();
367
+ }, wait);
368
+ }
369
+
370
+ #cancelScheduledFlush() {
371
+ if (this.#flushTimer) {
372
+ clearTimeout(this.#flushTimer);
373
+ this.#flushTimer = undefined;
310
374
  }
311
375
  }
312
376
 
@@ -319,45 +383,110 @@ export class DeltaStreamer<T> {
319
383
  // A provider can throw while responding to an abort. Join the durable
320
384
  // abort transition here, outside the active delta writer, before
321
385
  // preserving the provider error for the caller.
322
- await this.#abort(
323
- error instanceof Error ? error.message : "stream consumption failed",
324
- ).catch(() => {});
386
+ await this.#abort(errorToString(error)).catch(() => {});
325
387
  throw error;
326
388
  }
327
- // Skip finish if it will be handled externally (atomically with message save)
328
- // or if the stream was aborted (e.g., due to a failed delta write).
329
389
  // Abort cleanup owns the terminal component transition, so consumeStream
330
390
  // must wait for it instead of also trying to finish the stream.
331
391
  if (this.abortController.signal.aborted) {
332
392
  await this.#waitForAbortCleanup();
333
- } else if (!this.#finishedExternally) {
393
+ return;
394
+ }
395
+ // EOF is the only point where every part has actually been handed over:
396
+ // parts sit in the AI SDK's transform and tee pipeline until the iterator
397
+ // yields them, so no earlier callback can observe them.
398
+ try {
399
+ await this.#flushPendingParts();
400
+ } catch (error) {
401
+ if (this.abortController.signal.aborted) {
402
+ await this.#waitForAbortCleanup();
403
+ }
404
+ throw error;
405
+ }
406
+ if (this.abortController.signal.aborted) {
407
+ await this.#waitForAbortCleanup();
408
+ return;
409
+ }
410
+ if (!this.#finishHandledExternally) {
334
411
  await this.finish();
335
412
  }
336
413
  }
337
414
 
338
415
  /**
339
- * Mark the stream as being finished externally (e.g., atomically via addMessages).
340
- * When called, consumeStream() will skip calling finish() since it will be
341
- * handled elsewhere in the same mutation as message saving.
416
+ * Drain everything currently buffered or in flight, so that after this
417
+ * resolves no delta write is outstanding and #nextParts is empty.
342
418
  */
343
- public markFinishedExternally(): void {
344
- this.#finishedExternally = true;
419
+ async #flushPendingParts(): Promise<void> {
420
+ while (!this.abortController.signal.aborted) {
421
+ const inFlight = this.#ongoingWrite;
422
+ await inFlight;
423
+ // #sendDelta reassigns #ongoingWrite from its own tail, so a write can
424
+ // still be live even though the buffer it drained is now empty.
425
+ if (this.#ongoingWrite !== inFlight) {
426
+ continue;
427
+ }
428
+ if (this.#nextParts.length === 0) {
429
+ break;
430
+ }
431
+ this.#ongoingWrite = this.#sendDelta();
432
+ }
433
+ }
434
+
435
+ /**
436
+ * For the `returnImmediately` path, where nothing awaits consumption and the
437
+ * save has to happen inline (issue #265).
438
+ *
439
+ * Inherent window: parts still inside the AI SDK pipeline at this instant
440
+ * never reach addParts, so they are never persisted as deltas (the
441
+ * stream-level `finish` chunk among them). The message saved alongside this
442
+ * transition is complete, and is authoritative once the stream is finished.
443
+ *
444
+ * Narrower race left open: a part admitted after the loop's last emptiness
445
+ * check is buffered but not drained here, and the write it triggers at EOF
446
+ * lands on a row the caller has since finished. `#sendDelta` tolerates that
447
+ * refusal rather than aborting. Closing it means refusing admission for the
448
+ * duration of the drain, which drops more of the tail than it saves.
449
+ */
450
+ public async flushAndStopAccepting(): Promise<void> {
451
+ this.#cancelScheduledFlush();
452
+ await this.#flushPendingParts();
453
+ this.#stoppedAccepting = true;
345
454
  }
346
455
 
347
456
  /**
348
457
  * Get the stream ID, waiting for it to be created if necessary.
349
458
  * Useful for passing to addMessages for atomic finish.
350
459
  */
351
- public async getOrCreateStreamId(): Promise<string> {
352
- return this.getStreamId();
460
+ public async getOrCreateStreamId(): Promise<string>;
461
+ public async getOrCreateStreamId(options: {
462
+ ifAborted: "returnUndefined";
463
+ }): Promise<string | undefined>;
464
+ public async getOrCreateStreamId(options?: {
465
+ ifAborted?: "returnUndefined";
466
+ }): Promise<string | undefined> {
467
+ if (options?.ifAborted !== "returnUndefined") {
468
+ return this.getStreamId();
469
+ }
470
+ if (this.abortController.signal.aborted) {
471
+ await this.#abortPromise;
472
+ return undefined;
473
+ }
474
+ const streamId = await this.getStreamId();
475
+ if (this.abortController.signal.aborted) {
476
+ await this.#abortPromise;
477
+ return undefined;
478
+ }
479
+ return streamId;
353
480
  }
354
481
 
355
482
  async #sendDelta() {
483
+ this.#cancelScheduledFlush();
356
484
  if (this.abortController.signal.aborted) {
357
485
  return;
358
486
  }
359
487
  let success: boolean;
360
488
  try {
489
+ await this.getStreamId();
361
490
  const delta = await this.#createDelta();
362
491
  if (!delta) {
363
492
  return;
@@ -368,15 +497,15 @@ export class DeltaStreamer<T> {
368
497
  delta,
369
498
  );
370
499
  } catch (e) {
371
- await this.#abortDelta(e instanceof Error ? e.message : "unknown error");
500
+ await this.#abortDelta(errorToString(e));
372
501
  return;
373
502
  }
374
503
  if (!success) {
375
- // An in-flight #sendDelta started before markFinishedExternally()
504
+ // A #sendDelta racing the inline save on the returnImmediately path
376
505
  // will get `success === false` because the stream row is already
377
506
  // "finished". That's a benign late-write miss, not a failure —
378
507
  // don't convert it into an abort.
379
- if (this.#finishedExternally) {
508
+ if (this.#stoppedAccepting) {
380
509
  return;
381
510
  }
382
511
  await this.#abortDelta("async abort");
@@ -391,6 +520,11 @@ export class DeltaStreamer<T> {
391
520
  this.#ongoingWrite = this.#sendDelta();
392
521
  } else {
393
522
  this.#ongoingWrite = undefined;
523
+ // Whatever is still buffered has just lost its owner: this write is
524
+ // over and no arrival is guaranteed to follow. Hand it to the timer.
525
+ if (this.#nextParts.length > 0) {
526
+ this.#scheduleFlush();
527
+ }
394
528
  }
395
529
  }
396
530
 
@@ -427,6 +561,7 @@ export class DeltaStreamer<T> {
427
561
  }
428
562
 
429
563
  public async finish() {
564
+ this.#cancelScheduledFlush();
430
565
  if (!this.streamId) {
431
566
  return;
432
567
  }
@@ -471,6 +606,7 @@ export class DeltaStreamer<T> {
471
606
 
472
607
  #abort(reason: string, waitForOngoingWrite = true): Promise<void> {
473
608
  if (!this.#abortPromise) {
609
+ this.#cancelScheduledFlush();
474
610
  this.abortController.abort();
475
611
  this.#abortPromise = this.#abortCreatedStream(
476
612
  reason,
@@ -203,6 +203,8 @@ export type ContextOptions = {
203
203
  /**
204
204
  * How many recent messages to include. These are added after the search
205
205
  * messages, and do not count against the search limit.
206
+ * If the limit splits an order, additional messages are included to complete
207
+ * that order. If it cannot be completed, the incomplete order is omitted.
206
208
  * Default: 100
207
209
  */
208
210
  recentMessages?: number;
@@ -0,0 +1,97 @@
1
+ import type { StepResult } from "ai";
2
+ import { describe, expect, test } from "vitest";
3
+ import { hasSuccessfulToolCall, willContinue } from "./utils.js";
4
+
5
+ // Minimal StepResult builder — only the fields willContinue and
6
+ // hasSuccessfulToolCall actually read. Loosely typed on purpose so test
7
+ // fixtures can be terse; cast at the boundary.
8
+ type StepFixture = {
9
+ finishReason?: string;
10
+ content?: Array<{ type: string; toolName?: string }>;
11
+ toolCalls?: Array<{ toolCallId: string; toolName: string }>;
12
+ toolResults?: Array<{ toolCallId: string; toolName: string }>;
13
+ };
14
+
15
+ function makeStep(partial: StepFixture): StepResult<any> {
16
+ return {
17
+ finishReason: "tool-calls",
18
+ content: [],
19
+ toolCalls: [],
20
+ toolResults: [],
21
+ ...partial,
22
+ } as unknown as StepResult<any>;
23
+ }
24
+
25
+ describe("hasSuccessfulToolCall", () => {
26
+ test("returns true when last step has a tool-result for the named tool", () => {
27
+ const step = makeStep({
28
+ content: [{ type: "tool-result", toolName: "search" }],
29
+ });
30
+ expect(hasSuccessfulToolCall("search")({ steps: [step] })).toBe(true);
31
+ });
32
+
33
+ test("returns false when only a tool-error is present for the named tool", () => {
34
+ const step = makeStep({
35
+ content: [{ type: "tool-error", toolName: "search" }],
36
+ });
37
+ expect(hasSuccessfulToolCall("search")({ steps: [step] })).toBe(false);
38
+ });
39
+
40
+ test("returns false when the matching tool name is missing", () => {
41
+ const step = makeStep({
42
+ content: [{ type: "tool-result", toolName: "other" }],
43
+ });
44
+ expect(hasSuccessfulToolCall("search")({ steps: [step] })).toBe(false);
45
+ });
46
+
47
+ test("only inspects the last step", () => {
48
+ const earlier = makeStep({
49
+ content: [{ type: "tool-result", toolName: "search" }],
50
+ });
51
+ const last = makeStep({
52
+ content: [{ type: "tool-error", toolName: "search" }],
53
+ });
54
+ expect(hasSuccessfulToolCall("search")({ steps: [earlier, last] })).toBe(
55
+ false,
56
+ );
57
+ });
58
+
59
+ test("returns false when steps is empty", () => {
60
+ expect(hasSuccessfulToolCall("search")({ steps: [] })).toBe(false);
61
+ });
62
+ });
63
+
64
+ describe("willContinue", () => {
65
+ test("does not stop when a tool-error fills in for a missing tool-result", async () => {
66
+ // Two tool calls; one returns a result, the other errors.
67
+ const step = makeStep({
68
+ toolCalls: [
69
+ { toolCallId: "1", toolName: "a" },
70
+ { toolCallId: "2", toolName: "b" },
71
+ ],
72
+ toolResults: [{ toolCallId: "1", toolName: "a" }],
73
+ content: [
74
+ { type: "tool-result", toolName: "a" },
75
+ { type: "tool-error", toolName: "b" },
76
+ ],
77
+ });
78
+ // No stopWhen → returns false (no further stop conditions). The point
79
+ // is the function progresses past the early `toolCalls > completed`
80
+ // bail; pre-fix it returned early because tool-error wasn't counted.
81
+ expect(await willContinue([step], undefined)).toBe(false);
82
+ });
83
+
84
+ test("stops when a tool call has neither a result nor an error yet", async () => {
85
+ const step = makeStep({
86
+ toolCalls: [{ toolCallId: "1", toolName: "a" }],
87
+ toolResults: [],
88
+ content: [],
89
+ });
90
+ expect(await willContinue([step], undefined)).toBe(false);
91
+ });
92
+
93
+ test("stops when finishReason is not tool-calls", async () => {
94
+ const step = makeStep({ finishReason: "stop" });
95
+ expect(await willContinue([step], undefined)).toBe(false);
96
+ });
97
+ });
@@ -1,17 +1,23 @@
1
1
  import type { Context } from "@ai-sdk/provider-utils";
2
2
  import type { StepResult, StopCondition, ToolSet } from "ai";
3
3
 
4
+ export { errorToString } from "../../errors.js";
5
+
4
6
  /**
5
- * A stop condition that only matches tool calls which completed
6
- * successfully (i.e. produced a `tool-result`, not a `tool-error`).
7
+ * A stop condition that only matches tool calls of the given name which
8
+ * completed successfully i.e. produced a `tool-result` content part.
9
+ * Failed tool calls (which surface as `tool-error` parts under AI SDK v6)
10
+ * do not match.
7
11
  *
8
- * Use this instead of the AI SDK's `hasToolCall` when you want the
9
- * agent to retry on argument validation failures rather than stopping.
12
+ * Use this instead of the AI SDK's `hasToolCall` when you want the agent
13
+ * to retry on argument-validation or runtime tool failures rather than
14
+ * stopping. Evaluated only against the last step (consistent with how
15
+ * `stopWhen` is applied after each step).
10
16
  */
11
17
  export function hasSuccessfulToolCall(toolName: string): StopCondition<any> {
12
18
  return ({ steps }) =>
13
- steps[steps.length - 1]?.toolResults?.some(
14
- (result) => result.toolName === toolName,
19
+ steps[steps.length - 1]?.content?.some(
20
+ (p) => p.type === "tool-result" && p.toolName === toolName,
15
21
  ) ?? false;
16
22
  }
17
23
 
@@ -31,12 +37,13 @@ export async function willContinue<
31
37
  if (step.finishReason !== "tool-calls") return false;
32
38
  // Count both successful results and errors as completed outputs.
33
39
  // Failed tool calls are represented as tool-error content parts, so only
34
- // checking toolResults misses them.
40
+ // checking toolResults misses them. The fallback to step.toolResults.length
41
+ // is for callers whose steps lack content (mocks); the optional chain is
42
+ // defensive rather than load-bearing.
35
43
  const completedOutputs =
36
44
  step.content?.filter(
37
45
  (p) => p.type === "tool-result" || p.type === "tool-error",
38
46
  ).length ?? step.toolResults.length;
39
- // we don't have a tool result, so we'll wait for more
40
47
  if (step.toolCalls.length > completedOutputs) return false;
41
48
  if (Array.isArray(stopWhen)) {
42
49
  return (await Promise.all(stopWhen.map(async (s) => s({ steps })))).every(
@@ -45,10 +52,3 @@ export async function willContinue<
45
52
  }
46
53
  return !!stopWhen && !(await stopWhen({ steps }));
47
54
  }
48
-
49
- export function errorToString(error: unknown): string {
50
- if (error instanceof Error) {
51
- return error.message;
52
- }
53
- return String(error);
54
- }
@@ -818,6 +818,7 @@ export class Agent<
818
818
  metadata: args.metadata ? [args.metadata] : undefined,
819
819
  skipEmbeddings: args.skipEmbeddings,
820
820
  promptMessageId: args.promptMessageId,
821
+ order: args.order,
821
822
  pendingMessageId: args.pendingMessageId,
822
823
  });
823
824
  const message = messages.at(-1)!;
@@ -1765,6 +1766,7 @@ export class Agent<
1765
1766
  threadId: v.string(),
1766
1767
  userId: v.optional(v.string()),
1767
1768
  promptMessageId: v.optional(v.string()),
1769
+ order: v.optional(v.union(v.number(), v.literal("next"))),
1768
1770
  messages: v.array(vMessageWithMetadata),
1769
1771
  failPendingSteps: v.optional(v.boolean()),
1770
1772
  embeddings: v.optional(vMessageEmbeddings),