@ai-sdk/workflow 1.0.66 → 1.0.68

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.
@@ -0,0 +1,79 @@
1
+ import { APICallError } from '@ai-sdk/provider';
2
+ import {
3
+ WORKFLOW_DESERIALIZE,
4
+ WORKFLOW_SERIALIZE,
5
+ } from '@ai-sdk/provider-utils';
6
+ import { convertArrayToReadableStream, MockLanguageModelV4 } from 'ai/test';
7
+ import { getStepMetadata } from 'workflow';
8
+
9
+ type MockStreamResult = Awaited<ReturnType<MockLanguageModelV4['doStream']>>;
10
+ type MockStreamPart = MockStreamResult extends {
11
+ stream: ReadableStream<infer PART>;
12
+ }
13
+ ? PART
14
+ : never;
15
+
16
+ class SerializableRetryingModel extends MockLanguageModelV4 {
17
+ static [WORKFLOW_SERIALIZE](model: SerializableRetryingModel) {
18
+ return { failuresBeforeSuccess: model.failuresBeforeSuccess };
19
+ }
20
+
21
+ static [WORKFLOW_DESERIALIZE](options: { failuresBeforeSuccess: number }) {
22
+ return new SerializableRetryingModel(options.failuresBeforeSuccess);
23
+ }
24
+
25
+ constructor(readonly failuresBeforeSuccess: number) {
26
+ let modelAttempts = 0;
27
+
28
+ super({
29
+ provider: 'workflow-retry-test',
30
+ modelId: 'workflow-retry-test-model',
31
+ doStream: async () => {
32
+ modelAttempts++;
33
+
34
+ if (modelAttempts <= failuresBeforeSuccess) {
35
+ throw new APICallError({
36
+ message: `model call failed on attempt ${modelAttempts}`,
37
+ url: 'https://example.com/model',
38
+ requestBodyValues: {},
39
+ statusCode: 500,
40
+ responseHeaders: { 'retry-after-ms': '0' },
41
+ });
42
+ }
43
+
44
+ const text = `model-attempts=${modelAttempts};step-attempt=${getStepMetadata().attempt}`;
45
+ const streamParts: MockStreamPart[] = [
46
+ { type: 'stream-start', warnings: [] },
47
+ { type: 'text-start', id: '1' },
48
+ { type: 'text-delta', id: '1', delta: text },
49
+ { type: 'text-end', id: '1' },
50
+ {
51
+ type: 'finish',
52
+ finishReason: { unified: 'stop', raw: 'stop' },
53
+ usage: {
54
+ inputTokens: {
55
+ total: 1,
56
+ noCache: 1,
57
+ cacheRead: undefined,
58
+ cacheWrite: undefined,
59
+ },
60
+ outputTokens: {
61
+ total: 1,
62
+ text: 1,
63
+ reasoning: undefined,
64
+ },
65
+ },
66
+ },
67
+ ];
68
+
69
+ return { stream: convertArrayToReadableStream(streamParts) };
70
+ },
71
+ });
72
+ }
73
+ }
74
+
75
+ // Keep construction outside the workflow module to avoid the SWC closure
76
+ // transformation issue tracked in https://github.com/vercel/workflow/issues/1365.
77
+ export function retryingModel(): MockLanguageModelV4 {
78
+ return new SerializableRetryingModel(2);
79
+ }
@@ -7,6 +7,7 @@ import type {
7
7
  } from '@ai-sdk/provider';
8
8
  import {
9
9
  getErrorMessage,
10
+ isAbortError,
10
11
  validateTypes,
11
12
  withUserAgentSuffix,
12
13
  type Context,
@@ -39,7 +40,6 @@ import {
39
40
  createRestrictedTelemetryDispatcher,
40
41
  collectToolApprovals,
41
42
  convertToLanguageModelPrompt,
42
- mergeAbortSignals,
43
43
  mergeCallbacks,
44
44
  standardizePrompt,
45
45
  validateApprovedToolApprovals,
@@ -212,8 +212,7 @@ export interface GenerationSettings {
212
212
  seed?: number;
213
213
 
214
214
  /**
215
- * Maximum number of retries. Set to 0 to disable retries.
216
- * Note: In workflow context, retries are typically handled by the workflow step mechanism.
215
+ * Maximum number of retries for retryable model call failures. Set to 0 to disable retries.
217
216
  * @default 2
218
217
  */
219
218
  maxRetries?: number;
@@ -1191,6 +1190,15 @@ export interface WorkflowAgentStreamResult<
1191
1190
  */
1192
1191
  finishReason: FinishReason;
1193
1192
 
1193
+ /**
1194
+ * The original value from a model stream error part.
1195
+ *
1196
+ * This property is present when the model emitted an error part, including
1197
+ * when the supplied value is `undefined`. Check with `'error' in result` to
1198
+ * distinguish that case from a result without a model stream error.
1199
+ */
1200
+ error?: unknown;
1201
+
1194
1202
  /**
1195
1203
  * The total token usage across all steps.
1196
1204
  */
@@ -1788,10 +1796,10 @@ export class WorkflowAgent<
1788
1796
  download,
1789
1797
  });
1790
1798
 
1791
- const effectiveAbortSignal = mergeAbortSignals(
1792
- options.abortSignal ?? effectiveGenerationSettings.abortSignal,
1793
- options.timeout,
1794
- );
1799
+ const effectiveAbortSignal =
1800
+ options.abortSignal ?? effectiveGenerationSettings.abortSignal;
1801
+ const timeoutAt =
1802
+ options.timeout == null ? undefined : Date.now() + options.timeout;
1795
1803
 
1796
1804
  // Merge generation settings: constructor defaults < prepareCall < stream options
1797
1805
  const mergedGenerationSettings: GenerationSettings = {
@@ -2146,7 +2154,6 @@ export class WorkflowAgent<
2146
2154
  stopConditions: effectiveStopWhenFromPrepare,
2147
2155
  onStepEnd: mergedOnStepEnd as any,
2148
2156
  onStepStart: mergedOnStepStart as any,
2149
- onError: options.onError,
2150
2157
  prepareStep: (options.prepareStep ??
2151
2158
  (this.prepareStep as
2152
2159
  | PrepareStepCallback<ToolSet, TRuntimeContext>
@@ -2157,6 +2164,7 @@ export class WorkflowAgent<
2157
2164
  toolsContext,
2158
2165
  telemetry: effectiveTelemetry,
2159
2166
  includeRawChunks: options.includeRawChunks ?? false,
2167
+ timeoutAt,
2160
2168
  repairToolCall: (options.repairToolCall ??
2161
2169
  options.experimental_repairToolCall ??
2162
2170
  this.repairToolCall) as ToolCallRepairFunction<ToolSet> | undefined,
@@ -2167,7 +2175,10 @@ export class WorkflowAgent<
2167
2175
  // Track the final conversation messages from the iterator
2168
2176
  let finalMessages: LanguageModelV4Prompt | undefined;
2169
2177
  let encounteredError: unknown;
2178
+ let hasEncounteredError = false;
2170
2179
  let wasAborted = false;
2180
+ let terminalError: unknown;
2181
+ let hasTerminalError = false;
2171
2182
 
2172
2183
  try {
2173
2184
  let result = await iterator.next();
@@ -2519,14 +2530,29 @@ export class WorkflowAgent<
2519
2530
  }
2520
2531
  }
2521
2532
 
2522
- // When the iterator completes normally, result.value contains the final conversation prompt
2533
+ // When the iterator completes normally, result.value contains the final
2534
+ // conversation prompt. Aborts inside the retryable model step are
2535
+ // returned as data so the workflow runtime does not retry them.
2523
2536
  if (result.done) {
2524
- finalMessages = result.value;
2537
+ if (Array.isArray(result.value)) {
2538
+ finalMessages = result.value;
2539
+ } else if ('error' in result.value) {
2540
+ finalMessages = result.value.messages;
2541
+ terminalError = result.value.error;
2542
+ hasTerminalError = true;
2543
+ } else {
2544
+ finalMessages = result.value.messages;
2545
+ wasAborted = true;
2546
+ if (options.onAbort) {
2547
+ await options.onAbort({ steps });
2548
+ }
2549
+ }
2525
2550
  }
2526
2551
  } catch (error) {
2527
2552
  encounteredError = error;
2553
+ hasEncounteredError = true;
2528
2554
  // Check if this is an abort error
2529
- if (error instanceof Error && error.name === 'AbortError') {
2555
+ if (isAbortError(error)) {
2530
2556
  wasAborted = true;
2531
2557
  if (options.onAbort) {
2532
2558
  await options.onAbort({ steps });
@@ -2539,6 +2565,13 @@ export class WorkflowAgent<
2539
2565
  // Don't throw yet - we want to call onEnd first
2540
2566
  }
2541
2567
 
2568
+ if (hasTerminalError) {
2569
+ if (options.onError) {
2570
+ await options.onError({ error: terminalError });
2571
+ }
2572
+ await telemetryDispatcher.onError?.(terminalError);
2573
+ }
2574
+
2542
2575
  // Use the final messages from the iterator, or fall back to standardized messages
2543
2576
  const messages = (finalMessages ??
2544
2577
  prompt.messages) as unknown as ModelMessage[];
@@ -2562,8 +2595,9 @@ export class WorkflowAgent<
2562
2595
  } catch (parseError) {
2563
2596
  // If there's already an error, don't override it
2564
2597
  // If not, set this as the error
2565
- if (!encounteredError) {
2598
+ if (!hasEncounteredError) {
2566
2599
  encounteredError = parseError;
2600
+ hasEncounteredError = true;
2567
2601
  }
2568
2602
  }
2569
2603
  }
@@ -2599,7 +2633,7 @@ export class WorkflowAgent<
2599
2633
  }
2600
2634
 
2601
2635
  // Re-throw any error that occurred
2602
- if (encounteredError) {
2636
+ if (hasEncounteredError) {
2603
2637
  // Close the stream before throwing
2604
2638
  if (options.writable) {
2605
2639
  const sendFinish = options.sendFinish ?? true;
@@ -2628,6 +2662,7 @@ export class WorkflowAgent<
2628
2662
  finishReason,
2629
2663
  totalUsage,
2630
2664
  output: experimentalOutput,
2665
+ ...(hasTerminalError ? { error: terminalError } : {}),
2631
2666
  };
2632
2667
  }
2633
2668
  }