@ai-sdk/workflow 2.0.57 → 2.0.59

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.
@@ -18,6 +18,9 @@ import {
18
18
  } from '@ai-sdk/provider-utils';
19
19
  import {
20
20
  Output,
21
+ isStepCount,
22
+ NoOutputGeneratedError,
23
+ type GenerateTextResult,
21
24
  experimental_filterActiveTools as filterActiveTools,
22
25
  type FinishReason,
23
26
  type LanguageModelResponseMetadata,
@@ -41,6 +44,9 @@ import {
41
44
  } from 'ai';
42
45
  import {
43
46
  createRestrictedTelemetryDispatcher,
47
+ DefaultGenerateTextResult,
48
+ addLanguageModelUsage,
49
+ createNullLanguageModelUsage,
44
50
  collectToolApprovals,
45
51
  convertToLanguageModelPrompt,
46
52
  mergeAbortSignals,
@@ -59,7 +65,14 @@ import type {
59
65
  ProviderExecutedToolResult,
60
66
  } from './do-stream-step.js';
61
67
  import { resolveToolContext } from './resolve-tool-context.js';
62
- import { streamTextIterator } from './stream-text-iterator.js';
68
+ import { toModelResponseMessages } from './to-model-response-messages.js';
69
+ import { modelCallIterator } from './model-call-iterator.js';
70
+ import {
71
+ resolveWorkflowStreamResult,
72
+ type WorkflowExecutionData,
73
+ type WorkflowExecutionResult,
74
+ type WorkflowExecutionOutcome,
75
+ } from './workflow-execution-result.js';
63
76
 
64
77
  // Re-export for consumers
65
78
  export type { CompatibleLanguageModel } from './types.js';
@@ -921,7 +934,7 @@ export type WorkflowAgentOnToolExecutionEndCallback<
921
934
  /**
922
935
  * Options for the {@link WorkflowAgent.stream} method.
923
936
  */
924
- export type WorkflowAgentStreamOptions<
937
+ export type WorkflowAgentCallOptions<
925
938
  TTools extends ToolSet = ToolSet,
926
939
  TRuntimeContext extends Context = Context,
927
940
  OUTPUT = never,
@@ -960,7 +973,7 @@ export type WorkflowAgentStreamOptions<
960
973
  }
961
974
  ) & {
962
975
  /**
963
- * Instructions override for this stream call.
976
+ * Instructions override for this call.
964
977
  */
965
978
  instructions?: Instructions;
966
979
 
@@ -971,30 +984,6 @@ export type WorkflowAgentStreamOptions<
971
984
  */
972
985
  system?: string;
973
986
 
974
- /**
975
- * A WritableStream that receives raw LanguageModelV4StreamPart chunks in real-time
976
- * as the model generates them. This enables streaming to the client without
977
- * coupling WorkflowAgent to UIMessageChunk format.
978
- *
979
- * Convert to UIMessageChunks at the response boundary using
980
- * `createUIMessageChunkTransform()` from `@ai-sdk/workflow`.
981
- *
982
- * @example
983
- * ```typescript
984
- * // In the workflow:
985
- * await agent.stream({
986
- * messages,
987
- * writable: getWritable<ModelCallStreamPart>(),
988
- * });
989
- *
990
- * // In the route handler:
991
- * return createUIMessageStreamResponse({
992
- * stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
993
- * });
994
- * ```
995
- */
996
- writable?: WritableStream<ModelCallStreamPart<ToolSet>>;
997
-
998
987
  /**
999
988
  * Condition for stopping the generation when there are tool results in the last step.
1000
989
  * When the condition is an array, any of the conditions can be met to stop the generation.
@@ -1070,14 +1059,6 @@ export type WorkflowAgentStreamOptions<
1070
1059
  */
1071
1060
  output?: OutputSpecification<OUTPUT, PARTIAL_OUTPUT>;
1072
1061
 
1073
- /**
1074
- * Whether to include raw chunks from the provider in the stream.
1075
- * When enabled, you will receive raw chunks with type 'raw' that contain the unprocessed data from the provider.
1076
- * This allows access to cutting-edge provider features not yet wrapped by the AI SDK.
1077
- * Defaults to false.
1078
- */
1079
- includeRawChunks?: boolean;
1080
-
1081
1062
  /**
1082
1063
  * A function that attempts to repair a tool call that failed to parse.
1083
1064
  */
@@ -1090,15 +1071,6 @@ export type WorkflowAgentStreamOptions<
1090
1071
  */
1091
1072
  experimental_repairToolCall?: ToolCallRepairFunction<TTools>;
1092
1073
 
1093
- /**
1094
- * Optional stream transformations.
1095
- * They are applied in the order they are provided.
1096
- * The stream transformations must maintain the stream structure for streamText to work correctly.
1097
- */
1098
- experimental_transform?:
1099
- | StreamTextTransform<TTools>
1100
- | Array<StreamTextTransform<TTools>>;
1101
-
1102
1074
  /**
1103
1075
  * Custom download function to use for URLs.
1104
1076
  * By default, files are downloaded if the model does not support the URL for the given media type.
@@ -1221,24 +1193,113 @@ export type WorkflowAgentStreamOptions<
1221
1193
  prepareStep?: PrepareStepCallback<TTools, TRuntimeContext>;
1222
1194
 
1223
1195
  /**
1224
- * Timeout in milliseconds for the stream operation.
1196
+ * Timeout in milliseconds for model calls in this operation.
1225
1197
  * When specified, creates an AbortSignal that will abort the operation after the given time.
1226
1198
  * If both `timeout` and `abortSignal` are provided, whichever triggers first will abort.
1227
1199
  */
1228
1200
  timeout?: number;
1201
+ };
1229
1202
 
1230
- /**
1231
- * Whether to send a 'finish' chunk to the writable stream when streaming completes.
1232
- * @default true
1233
- */
1234
- sendFinish?: boolean;
1203
+ /** Streaming options add transport controls to the shared call options. */
1204
+ export type WorkflowAgentStreamOptions<
1205
+ TTools extends ToolSet = ToolSet,
1206
+ TRuntimeContext extends Context = Context,
1207
+ OUTPUT = never,
1208
+ PARTIAL_OUTPUT = never,
1209
+ > = WorkflowAgentCallOptions<
1210
+ TTools,
1211
+ TRuntimeContext,
1212
+ OUTPUT,
1213
+ PARTIAL_OUTPUT
1214
+ > & {
1215
+ /**
1216
+ * A WritableStream that receives raw LanguageModelV4StreamPart chunks in real-time
1217
+ * as the model generates them. This enables streaming to the client without
1218
+ * coupling WorkflowAgent to UIMessageChunk format.
1219
+ *
1220
+ * Convert to UIMessageChunks at the response boundary using
1221
+ * `createUIMessageChunkTransform()` from `@ai-sdk/workflow`.
1222
+ *
1223
+ * @example
1224
+ * ```typescript
1225
+ * // In the workflow:
1226
+ * await agent.stream({
1227
+ * messages,
1228
+ * writable: getWritable<ModelCallStreamPart>(),
1229
+ * });
1230
+ *
1231
+ * // In the route handler:
1232
+ * return createUIMessageStreamResponse({
1233
+ * stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
1234
+ * });
1235
+ * ```
1236
+ */
1237
+ writable?: WritableStream<ModelCallStreamPart<ToolSet>>;
1235
1238
 
1236
- /**
1237
- * Whether to prevent the writable stream from being closed after streaming completes.
1238
- * @default false
1239
- */
1240
- preventClose?: boolean;
1239
+ /**
1240
+ * Whether to include raw chunks from the provider in the stream.
1241
+ * When enabled, you will receive raw chunks with type 'raw' that contain the unprocessed data from the provider.
1242
+ * This allows access to cutting-edge provider features not yet wrapped by the AI SDK.
1243
+ * Defaults to false.
1244
+ */
1245
+ includeRawChunks?: boolean;
1246
+
1247
+ /**
1248
+ * Optional stream transformations.
1249
+ * They are applied in the order they are provided.
1250
+ * The stream transformations must maintain the stream structure for streamText to work correctly.
1251
+ */
1252
+ experimental_transform?:
1253
+ | StreamTextTransform<TTools>
1254
+ | Array<StreamTextTransform<TTools>>;
1255
+
1256
+ /**
1257
+ * Whether to send a 'finish' chunk to the writable stream when streaming completes.
1258
+ * @default true
1259
+ */
1260
+ sendFinish?: boolean;
1261
+
1262
+ /**
1263
+ * Whether to prevent the writable stream from being closed after streaming completes.
1264
+ * @default false
1265
+ */
1266
+ preventClose?: boolean;
1267
+ };
1268
+
1269
+ export type WorkflowAgentGenerateOptions<
1270
+ TTools extends ToolSet = ToolSet,
1271
+ TRuntimeContext extends Context = Context,
1272
+ OUTPUT = string,
1273
+ PARTIAL_OUTPUT = never,
1274
+ > = WorkflowAgentCallOptions<
1275
+ TTools,
1276
+ TRuntimeContext,
1277
+ OUTPUT,
1278
+ PARTIAL_OUTPUT
1279
+ > & {
1280
+ /** Request/response bodies and request messages are omitted by default. */
1281
+ include?: {
1282
+ requestBody?: boolean;
1283
+ responseBody?: boolean;
1284
+ requestMessages?: boolean;
1241
1285
  };
1286
+ };
1287
+
1288
+ /** Core result semantics, available inside the workflow that runs generation. */
1289
+ export type WorkflowAgentGenerateResult<
1290
+ TTools extends ToolSet = ToolSet,
1291
+ TRuntimeContext extends Context = Context,
1292
+ OUTPUT = string,
1293
+ > = Omit<
1294
+ GenerateTextResult<TTools, TRuntimeContext, Output.Output>,
1295
+ 'output'
1296
+ > & { readonly output: OUTPUT };
1297
+
1298
+ type DefaultGenerateOutput<OUTPUT> = [
1299
+ DefaultWorkflowAgentOutput<OUTPUT>,
1300
+ ] extends [never]
1301
+ ? string
1302
+ : OUTPUT;
1242
1303
 
1243
1304
  /**
1244
1305
  * A tool call made by the model. Matches the AI SDK's tool call shape.
@@ -1279,6 +1340,7 @@ type WorkflowToolExecutionResult = {
1279
1340
  function addToolResultsToStep(
1280
1341
  step: StepResult<ToolSet, any> | undefined,
1281
1342
  executedResults: WorkflowToolExecutionResult[],
1343
+ mode: 'generate' | 'stream' = 'stream',
1282
1344
  ) {
1283
1345
  if (step == null || executedResults.length === 0) {
1284
1346
  return;
@@ -1306,6 +1368,14 @@ function addToolResultsToStep(
1306
1368
  toolCallId: result.modelResult.toolCallId,
1307
1369
  toolName: result.modelResult.toolName,
1308
1370
  input: toolCall?.input,
1371
+ ...(mode === 'generate'
1372
+ ? {
1373
+ dynamic: toolCall?.dynamic === true,
1374
+ ...(toolCall?.toolMetadata != null
1375
+ ? { toolMetadata: toolCall.toolMetadata }
1376
+ : {}),
1377
+ }
1378
+ : {}),
1309
1379
  ...(toolCall?.dynamic === true ? { dynamic: true as const } : {}),
1310
1380
  ...(toolCall?.providerExecuted === true
1311
1381
  ? { providerExecuted: true }
@@ -1329,6 +1399,9 @@ function addToolResultsToStep(
1329
1399
 
1330
1400
  step.content.push(...(toolOutputs as StepResult<ToolSet, any>['content']));
1331
1401
 
1402
+ // Core's generate step derives its tool lists from content getters.
1403
+ if (mode === 'generate') return;
1404
+
1332
1405
  const toolResults = toolOutputs.filter(
1333
1406
  result => result.type === 'tool-result',
1334
1407
  );
@@ -1555,8 +1628,54 @@ export class WorkflowAgent<
1555
1628
  };
1556
1629
  }
1557
1630
 
1558
- generate() {
1559
- throw new Error('Not implemented');
1631
+ async generate<
1632
+ TTools extends TBaseTools = TBaseTools,
1633
+ TOutput = DefaultGenerateOutput<OUTPUT>,
1634
+ TPartialOutput = DefaultWorkflowAgentOutput<PARTIAL_OUTPUT>,
1635
+ >(
1636
+ options: WorkflowAgentGenerateOptions<
1637
+ TTools,
1638
+ TRuntimeContext,
1639
+ TOutput,
1640
+ TPartialOutput
1641
+ >,
1642
+ ): Promise<WorkflowAgentGenerateResult<TTools, TRuntimeContext, TOutput>> {
1643
+ const {
1644
+ data,
1645
+ outcome,
1646
+ abortReason,
1647
+ initialResponseMessages = [],
1648
+ } = await this.execute(options, 'generate');
1649
+ if (outcome.status === 'failed') throw outcome.error;
1650
+ if (outcome.status === 'aborted') {
1651
+ if (outcome.rejection != null) throw outcome.rejection.value;
1652
+ if (abortReason != null) throw abortReason.value;
1653
+ options.abortSignal?.throwIfAborted();
1654
+ throw new DOMException('Generation aborted.', 'AbortError');
1655
+ }
1656
+ const lastStep = data.steps.at(-1);
1657
+ if (lastStep == null) throw new NoOutputGeneratedError();
1658
+ let output: TOutput | undefined;
1659
+ if (
1660
+ lastStep.finishReason === 'stop' ||
1661
+ (lastStep.finishReason !== 'tool-calls' && lastStep.text.length > 0)
1662
+ ) {
1663
+ const specification = options.output ?? this.output ?? Output.text();
1664
+ output = (await specification.parseCompleteOutput(
1665
+ { text: lastStep.text },
1666
+ {
1667
+ response: lastStep.response,
1668
+ usage: lastStep.usage,
1669
+ finishReason: lastStep.finishReason,
1670
+ },
1671
+ )) as TOutput;
1672
+ }
1673
+ return new DefaultGenerateTextResult({
1674
+ initialResponseMessages,
1675
+ steps: data.steps as StepResult<TTools, TRuntimeContext>[],
1676
+ totalUsage: data.totalUsage,
1677
+ output,
1678
+ }) as WorkflowAgentGenerateResult<TTools, TRuntimeContext, TOutput>;
1560
1679
  }
1561
1680
 
1562
1681
  async stream<
@@ -1571,6 +1690,22 @@ export class WorkflowAgent<
1571
1690
  TPartialOutput
1572
1691
  >,
1573
1692
  ): Promise<WorkflowAgentStreamResult<TTools, TOutput>> {
1693
+ return resolveWorkflowStreamResult(await this.execute(options));
1694
+ }
1695
+
1696
+ private async prepareInvocation<
1697
+ TTools extends TBaseTools = TBaseTools,
1698
+ TOutput = DefaultWorkflowAgentOutput<OUTPUT>,
1699
+ TPartialOutput = DefaultWorkflowAgentOutput<PARTIAL_OUTPUT>,
1700
+ >(
1701
+ options: WorkflowAgentStreamOptions<
1702
+ TTools,
1703
+ TRuntimeContext,
1704
+ TOutput,
1705
+ TPartialOutput
1706
+ > & { include?: WorkflowAgentGenerateOptions['include'] },
1707
+ mode: 'generate' | 'stream' = 'stream',
1708
+ ) {
1574
1709
  const { onFinish, onEnd = onFinish } = options;
1575
1710
 
1576
1711
  // Call prepareCall to transform parameters before the agent loop
@@ -1590,7 +1725,10 @@ export class WorkflowAgent<
1590
1725
  Context | undefined
1591
1726
  >;
1592
1727
  let effectiveToolChoiceFromPrepare = options.toolChoice ?? this.toolChoice;
1593
- let effectiveStopWhenFromPrepare = options.stopWhen ?? this.stopWhen;
1728
+ let effectiveStopWhenFromPrepare =
1729
+ options.stopWhen ??
1730
+ this.stopWhen ??
1731
+ (mode === 'generate' ? isStepCount(20) : undefined);
1594
1732
  let effectiveActiveToolsFromPrepare =
1595
1733
  options.activeTools ?? this.activeTools;
1596
1734
  let effectiveDownloadFromPrepare =
@@ -1720,6 +1858,70 @@ export class WorkflowAgent<
1720
1858
  options.onToolExecutionEnd,
1721
1859
  );
1722
1860
 
1861
+ return {
1862
+ onEnd,
1863
+ effectiveModel,
1864
+ effectiveInstructions,
1865
+ effectiveGenerationSettings,
1866
+ effectiveRuntimeContext,
1867
+ effectiveToolsContext,
1868
+ effectiveToolChoiceFromPrepare,
1869
+ effectiveStopWhenFromPrepare,
1870
+ effectiveActiveToolsFromPrepare,
1871
+ effectiveToolApprovalSecret,
1872
+ effectiveTelemetry,
1873
+ telemetryDispatcher,
1874
+ prompt,
1875
+ download,
1876
+ sandbox,
1877
+ effectiveAbortSignal,
1878
+ timeoutAt,
1879
+ mergedOnToolExecutionStart,
1880
+ mergedOnToolExecutionEnd,
1881
+ };
1882
+ }
1883
+
1884
+ private async execute<
1885
+ TTools extends TBaseTools = TBaseTools,
1886
+ TOutput = DefaultWorkflowAgentOutput<OUTPUT>,
1887
+ TPartialOutput = DefaultWorkflowAgentOutput<PARTIAL_OUTPUT>,
1888
+ >(
1889
+ options: WorkflowAgentStreamOptions<
1890
+ TTools,
1891
+ TRuntimeContext,
1892
+ TOutput,
1893
+ TPartialOutput
1894
+ > & { include?: WorkflowAgentGenerateOptions['include'] },
1895
+ mode: 'generate' | 'stream' = 'stream',
1896
+ ): Promise<WorkflowExecutionResult<TTools, TOutput>> {
1897
+ const {
1898
+ onEnd,
1899
+ effectiveModel,
1900
+ effectiveInstructions,
1901
+ effectiveGenerationSettings,
1902
+ effectiveRuntimeContext,
1903
+ effectiveToolsContext,
1904
+ effectiveToolChoiceFromPrepare,
1905
+ effectiveStopWhenFromPrepare,
1906
+ effectiveActiveToolsFromPrepare,
1907
+ effectiveToolApprovalSecret,
1908
+ effectiveTelemetry,
1909
+ telemetryDispatcher,
1910
+ prompt,
1911
+ download,
1912
+ sandbox,
1913
+ effectiveAbortSignal,
1914
+ timeoutAt,
1915
+ mergedOnToolExecutionStart,
1916
+ mergedOnToolExecutionEnd,
1917
+ } = await this.prepareInvocation(options, mode);
1918
+ const initialResponseMessages: WorkflowAgentGenerateResult<TTools>['responseMessages'] =
1919
+ [];
1920
+ const abortReason = () =>
1921
+ effectiveAbortSignal?.reason === undefined
1922
+ ? undefined
1923
+ : { value: effectiveAbortSignal.reason };
1924
+
1723
1925
  // Process tool approval responses before starting the agent loop.
1724
1926
  // This mirrors how stream-text.ts handles tool-approval-response parts:
1725
1927
  // approved tools are executed, denied tools get denial results, and
@@ -1939,6 +2141,13 @@ export class WorkflowAgent<
1939
2141
  }
1940
2142
  }
1941
2143
 
2144
+ if (mode === 'generate' && toolResultContent.length > 0) {
2145
+ initialResponseMessages.push({
2146
+ role: 'tool',
2147
+ content: toolResultContent,
2148
+ } as unknown as WorkflowAgentGenerateResult<TTools>['responseMessages'][number]);
2149
+ }
2150
+
1942
2151
  // Add tool results as a new tool message
1943
2152
  if (toolResultContent.length > 0) {
1944
2153
  cleanedMessages.push({
@@ -2287,17 +2496,23 @@ export class WorkflowAgent<
2287
2496
  await options.onAbort({ steps });
2288
2497
  }
2289
2498
  return {
2290
- messages: prompt.messages,
2291
- steps,
2292
- toolCalls: [],
2293
- toolResults: [],
2294
- finishReason: 'other',
2295
- totalUsage: aggregateUsage(steps),
2296
- output: undefined as TOutput,
2499
+ outcome: { status: 'aborted' },
2500
+ abortReason: abortReason(),
2501
+ data: {
2502
+ messages: prompt.messages,
2503
+ steps,
2504
+ toolCalls: [],
2505
+ toolResults: [],
2506
+ finishReason: 'other',
2507
+ totalUsage: aggregateExecutionUsage(steps, mode),
2508
+ output: undefined as TOutput,
2509
+ },
2297
2510
  };
2298
2511
  }
2299
2512
 
2300
- const iterator = streamTextIterator({
2513
+ const iterator = modelCallIterator({
2514
+ mode,
2515
+ include: options.include,
2301
2516
  model: effectiveModel,
2302
2517
  tools: effectiveTools as ToolSet,
2303
2518
  writable: options.writable,
@@ -2363,6 +2578,19 @@ export class WorkflowAgent<
2363
2578
  providerExecutedToolResults,
2364
2579
  providerExecutedToolResultPositions,
2365
2580
  } = result.value;
2581
+ if (mode === 'generate' && step != null) {
2582
+ for (const call of toolCalls) {
2583
+ if (call.invalid && !call.providerExecuted)
2584
+ step.content.push({
2585
+ type: 'tool-error',
2586
+ toolCallId: call.toolCallId,
2587
+ toolName: call.toolName,
2588
+ input: call.input,
2589
+ error: call.error,
2590
+ dynamic: true,
2591
+ });
2592
+ }
2593
+ }
2366
2594
  const capturedProviderToolResults =
2367
2595
  providerExecutedToolResults ??
2368
2596
  new Map<string, ProviderExecutedToolResult>();
@@ -2461,7 +2689,14 @@ export class WorkflowAgent<
2461
2689
  // stop the loop and return them.
2462
2690
  // This matches AI SDK behavior: tools without execute or needing
2463
2691
  // approval pause the agent loop.
2464
- if (pausedToolCalls.length > 0) {
2692
+ const providerApprovalRequests =
2693
+ step?.content
2694
+ .filter(part => part.type === 'tool-approval-request')
2695
+ .filter(part => part.toolCall.providerExecuted) ?? [];
2696
+ if (
2697
+ pausedToolCalls.length > 0 ||
2698
+ providerApprovalRequests.length > 0
2699
+ ) {
2465
2700
  // Execute any executable tools that were also called in this step
2466
2701
  const executableResults = await Promise.all(
2467
2702
  executableToolCalls.map(
@@ -2479,15 +2714,23 @@ export class WorkflowAgent<
2479
2714
 
2480
2715
  // Collect provider tool results
2481
2716
  const providerResultEntries = await Promise.all(
2482
- providerToolCallsForResults.map(async toolCall => ({
2483
- toolCall,
2484
- result: await resolveProviderToolResult(
2717
+ providerToolCallsForResults
2718
+ .filter(
2719
+ call =>
2720
+ !providerApprovalRequests.some(
2721
+ request =>
2722
+ request.toolCall.toolCallId === call.toolCallId,
2723
+ ),
2724
+ )
2725
+ .map(async toolCall => ({
2485
2726
  toolCall,
2486
- capturedProviderToolResults,
2487
- stepTools,
2488
- download,
2489
- ),
2490
- })),
2727
+ result: await resolveProviderToolResult(
2728
+ toolCall,
2729
+ capturedProviderToolResults,
2730
+ stepTools,
2731
+ download,
2732
+ ),
2733
+ })),
2491
2734
  );
2492
2735
  await Promise.all(
2493
2736
  providerResultEntries.flatMap(({ toolCall, result }) =>
@@ -2534,7 +2777,45 @@ export class WorkflowAgent<
2534
2777
  output: r.rawOutput,
2535
2778
  }));
2536
2779
 
2537
- addToolResultsToStep(step, executedResults);
2780
+ addToolResultsToStep(step, executedResults, mode);
2781
+
2782
+ // Approval data belongs to the execution, whether or not it has a
2783
+ // writable. Only the environment-variable reference enters the
2784
+ // durable signing step; the resolved secret stays inside it.
2785
+ const approvalToolCalls = pausedToolCalls.filter(
2786
+ tc => approvalNeeded[nonProviderToolCalls.indexOf(tc)],
2787
+ );
2788
+ const approvalRequests = await Promise.all(
2789
+ approvalToolCalls.map(async tc => {
2790
+ const approvalId = `approval-${tc.toolCallId}`;
2791
+ const signature =
2792
+ effectiveToolApprovalSecret == null
2793
+ ? undefined
2794
+ : await signWorkflowToolApproval({
2795
+ secret: effectiveToolApprovalSecret,
2796
+ approvalId,
2797
+ toolCallId: tc.toolCallId,
2798
+ toolName: tc.toolName,
2799
+ input: tc.input,
2800
+ });
2801
+ return {
2802
+ type: 'tool-approval-request' as const,
2803
+ approvalId,
2804
+ toolCall: {
2805
+ ...tc,
2806
+ type: 'tool-call' as const,
2807
+ } as StepResult<ToolSet>['toolCalls'][number],
2808
+ ...(signature != null ? { signature } : {}),
2809
+ };
2810
+ }),
2811
+ );
2812
+ step?.content.push(...approvalRequests);
2813
+ const approvalMessages = approvalRequests.map(
2814
+ ({ toolCall, ...request }) => ({
2815
+ ...request,
2816
+ toolCallId: toolCall.toolCallId,
2817
+ }),
2818
+ );
2538
2819
 
2539
2820
  const responseMessages = addToolResultsToConversation({
2540
2821
  messages: iterMessages,
@@ -2546,42 +2827,59 @@ export class WorkflowAgent<
2546
2827
  ),
2547
2828
  providerExecutedToolResultPositions,
2548
2829
  });
2830
+ const publicResponseMessages = addApprovalRequestsToMessages(
2831
+ mode === 'generate'
2832
+ ? toModelResponseMessages(responseMessages)
2833
+ : (responseMessages as unknown as ModelMessage[]),
2834
+ [
2835
+ ...approvalMessages,
2836
+ ...providerApprovalRequests.map(({ toolCall, ...request }) => ({
2837
+ ...request,
2838
+ toolCallId: toolCall.toolCallId,
2839
+ })),
2840
+ ],
2841
+ );
2549
2842
  step?.response.messages.push(
2550
- ...(responseMessages as unknown as NonNullable<
2843
+ ...(publicResponseMessages as NonNullable<
2551
2844
  typeof step
2552
2845
  >['response']['messages']),
2553
2846
  );
2554
2847
 
2555
- const messages = iterMessages as unknown as ModelMessage[];
2848
+ // Approval requests are public conversation data, not provider
2849
+ // prompt parts. The next invocation consumes them before conversion.
2850
+ const messages = (
2851
+ publicResponseMessages === responseMessages
2852
+ ? iterMessages
2853
+ : [
2854
+ ...iterMessages.slice(
2855
+ 0,
2856
+ iterMessages.length - responseMessages.length,
2857
+ ),
2858
+ ...publicResponseMessages,
2859
+ ]
2860
+ ) as ModelMessage[];
2556
2861
  const lastStep = steps[steps.length - 1];
2557
- const totalUsage = aggregateUsage(steps);
2862
+ const totalUsage = aggregateExecutionUsage(steps, mode);
2558
2863
  const finishReason = lastStep?.finishReason ?? 'other';
2559
2864
 
2560
- if (mergedOnEnd && !wasAborted) {
2561
- await mergedOnEnd({
2562
- steps,
2563
- messages,
2564
- text: lastStep?.text ?? '',
2565
- finishReason,
2566
- usage: totalUsage,
2567
- totalUsage,
2568
- runtimeContext,
2569
- toolsContext:
2570
- toolsContext as unknown as InferToolSetContext<TTools>,
2571
- output: undefined as TOutput,
2572
- });
2573
- }
2574
- if (!wasAborted && steps.length > 0) {
2575
- const telemetrySteps = steps.map(normalizeStepForTelemetry);
2576
- const lastTelemetryStep =
2577
- telemetrySteps[telemetrySteps.length - 1];
2578
- await telemetryDispatcher.onEnd?.({
2579
- ...lastTelemetryStep,
2580
- steps: telemetrySteps,
2581
- usage: totalUsage,
2582
- totalUsage,
2583
- });
2865
+ const data: WorkflowExecutionData<TTools, TOutput> = {
2866
+ messages,
2867
+ steps,
2868
+ toolCalls: allToolCalls,
2869
+ toolResults: allToolResults,
2870
+ finishReason,
2871
+ totalUsage,
2872
+ output: undefined as TOutput,
2873
+ };
2874
+ if (mode === 'generate' && step != null) {
2875
+ await mergedOnStepEnd?.(
2876
+ step as StepResult<TTools, TRuntimeContext>,
2877
+ );
2878
+ await telemetryDispatcher.onStepEnd?.(
2879
+ normalizeStepForTelemetry(step),
2880
+ );
2584
2881
  }
2882
+ await notifyEnd(data, wasAborted);
2585
2883
 
2586
2884
  // Emit tool-approval-request chunks for tools that need approval
2587
2885
  // so useChat can show the approval UI
@@ -2601,58 +2899,16 @@ export class WorkflowAgent<
2601
2899
  );
2602
2900
  }
2603
2901
 
2604
- const approvalToolCalls = pausedToolCalls.filter((_, i) => {
2605
- const tcIndex = nonProviderToolCalls.indexOf(
2606
- pausedToolCalls[i],
2607
- );
2608
- return approvalNeeded[tcIndex];
2609
- });
2610
- if (approvalToolCalls.length > 0) {
2611
- // The signing step receives only a non-secret environment
2612
- // variable reference. It resolves the secret inside the step,
2613
- // and only the resulting signature is persisted in the stream.
2614
- const approvalRequests = await Promise.all(
2615
- approvalToolCalls.map(async tc => {
2616
- const approvalId = `approval-${tc.toolCallId}`;
2617
- const signature =
2618
- effectiveToolApprovalSecret == null
2619
- ? undefined
2620
- : await signWorkflowToolApproval({
2621
- secret: effectiveToolApprovalSecret,
2622
- approvalId,
2623
- toolCallId: tc.toolCallId,
2624
- toolName: tc.toolName,
2625
- input: tc.input,
2626
- });
2627
-
2628
- return {
2629
- approvalId,
2630
- toolCallId: tc.toolCallId,
2631
- ...(signature != null ? { signature } : {}),
2632
- };
2633
- }),
2634
- );
2635
- await writeApprovalRequests(options.writable, approvalRequests);
2636
- }
2637
- }
2638
-
2639
- // Close the stream before returning for paused tools
2640
- if (options.writable) {
2641
- const sendFinish = options.sendFinish ?? true;
2642
- const preventClose = options.preventClose ?? false;
2643
- if (sendFinish || !preventClose) {
2644
- await closeStream(options.writable, preventClose, sendFinish);
2902
+ if (approvalMessages.length > 0) {
2903
+ await writeApprovalRequests(options.writable, approvalMessages);
2645
2904
  }
2646
2905
  }
2647
2906
 
2907
+ await closeOutput();
2648
2908
  return {
2649
- messages,
2650
- steps,
2651
- toolCalls: allToolCalls,
2652
- toolResults: allToolResults,
2653
- finishReason,
2654
- totalUsage,
2655
- output: undefined as TOutput,
2909
+ data,
2910
+ outcome: { status: 'completed' },
2911
+ initialResponseMessages,
2656
2912
  };
2657
2913
  }
2658
2914
 
@@ -2781,7 +3037,7 @@ export class WorkflowAgent<
2781
3037
  output: r.rawOutput,
2782
3038
  }));
2783
3039
 
2784
- addToolResultsToStep(step, executedToolResults);
3040
+ addToolResultsToStep(step, executedToolResults, mode);
2785
3041
 
2786
3042
  result = await iterator.next(continuationToolResults);
2787
3043
  } else {
@@ -2843,7 +3099,7 @@ export class WorkflowAgent<
2843
3099
  | OutputSpecification<TOutput, TPartialOutput>
2844
3100
  | undefined;
2845
3101
  let experimentalOutput: TOutput = undefined as TOutput;
2846
- if (effectiveOutput && steps.length > 0) {
3102
+ if (mode === 'stream' && effectiveOutput && steps.length > 0) {
2847
3103
  const lastStep = steps[steps.length - 1];
2848
3104
  const text = lastStep.text;
2849
3105
  if (text) {
@@ -2868,37 +3124,68 @@ export class WorkflowAgent<
2868
3124
  }
2869
3125
 
2870
3126
  const lastStep = steps[steps.length - 1];
2871
- const totalUsage = aggregateUsage(steps);
3127
+ const totalUsage = aggregateExecutionUsage(steps, mode);
2872
3128
  const finishReason = lastStep?.finishReason ?? 'other';
2873
3129
 
2874
- // Call onEnd callback if provided (always call, even on errors, but not on abort)
2875
- if (mergedOnEnd && !wasAborted) {
2876
- await mergedOnEnd({
2877
- steps,
2878
- messages: messages as ModelMessage[],
2879
- text: lastStep?.text ?? '',
2880
- finishReason,
2881
- usage: totalUsage,
2882
- totalUsage,
2883
- runtimeContext,
2884
- toolsContext: toolsContext as unknown as InferToolSetContext<TTools>,
2885
- output: experimentalOutput,
2886
- });
2887
- }
2888
- if (!wasAborted && steps.length > 0) {
2889
- const telemetrySteps = steps.map(normalizeStepForTelemetry);
2890
- const lastTelemetryStep = telemetrySteps[telemetrySteps.length - 1];
2891
- await telemetryDispatcher.onEnd?.({
2892
- ...lastTelemetryStep,
2893
- steps: telemetrySteps,
2894
- usage: totalUsage,
2895
- totalUsage,
2896
- });
3130
+ const data: WorkflowExecutionData<TTools, TOutput> = {
3131
+ messages,
3132
+ steps,
3133
+ toolCalls: lastStepToolCalls,
3134
+ toolResults: lastStepToolResults,
3135
+ finishReason,
3136
+ totalUsage,
3137
+ output: experimentalOutput,
3138
+ };
3139
+ if (mode === 'stream' || (!hasEncounteredError && !hasTerminalError))
3140
+ await notifyEnd(data, wasAborted);
3141
+ await closeOutput();
3142
+
3143
+ const outcome: WorkflowExecutionOutcome = hasEncounteredError
3144
+ ? wasAborted
3145
+ ? { status: 'aborted', rejection: { value: encounteredError } }
3146
+ : { status: 'failed', source: 'execution', error: encounteredError }
3147
+ : hasTerminalError
3148
+ ? { status: 'failed', source: 'model-stream', error: terminalError }
3149
+ : wasAborted
3150
+ ? { status: 'aborted' }
3151
+ : { status: 'completed' };
3152
+ return {
3153
+ data,
3154
+ outcome,
3155
+ initialResponseMessages,
3156
+ ...(outcome.status === 'aborted' ? { abortReason: abortReason() } : {}),
3157
+ };
3158
+
3159
+ async function notifyEnd(
3160
+ data: WorkflowExecutionData<TTools, TOutput>,
3161
+ aborted: boolean,
3162
+ ) {
3163
+ if (aborted) return;
3164
+ if (mergedOnEnd) {
3165
+ await mergedOnEnd({
3166
+ steps: data.steps,
3167
+ messages: data.messages,
3168
+ text: data.steps.at(-1)?.text ?? '',
3169
+ finishReason: data.finishReason,
3170
+ usage: data.totalUsage,
3171
+ totalUsage: data.totalUsage,
3172
+ runtimeContext,
3173
+ toolsContext: toolsContext as unknown as InferToolSetContext<TTools>,
3174
+ output: data.output,
3175
+ });
3176
+ }
3177
+ if (data.steps.length > 0) {
3178
+ const telemetrySteps = data.steps.map(normalizeStepForTelemetry);
3179
+ await telemetryDispatcher.onEnd?.({
3180
+ ...telemetrySteps[telemetrySteps.length - 1],
3181
+ steps: telemetrySteps,
3182
+ usage: data.totalUsage,
3183
+ totalUsage: data.totalUsage,
3184
+ });
3185
+ }
2897
3186
  }
2898
3187
 
2899
- // Re-throw any error that occurred
2900
- if (hasEncounteredError) {
2901
- // Close the stream before throwing
3188
+ async function closeOutput() {
2902
3189
  if (options.writable) {
2903
3190
  const sendFinish = options.sendFinish ?? true;
2904
3191
  const preventClose = options.preventClose ?? false;
@@ -2906,28 +3193,7 @@ export class WorkflowAgent<
2906
3193
  await closeStream(options.writable, preventClose, sendFinish);
2907
3194
  }
2908
3195
  }
2909
- throw encounteredError;
2910
3196
  }
2911
-
2912
- // Close the writable stream
2913
- if (options.writable) {
2914
- const sendFinish = options.sendFinish ?? true;
2915
- const preventClose = options.preventClose ?? false;
2916
- if (sendFinish || !preventClose) {
2917
- await closeStream(options.writable, preventClose, sendFinish);
2918
- }
2919
- }
2920
-
2921
- return {
2922
- messages: messages as ModelMessage[],
2923
- steps,
2924
- toolCalls: lastStepToolCalls,
2925
- toolResults: lastStepToolResults,
2926
- finishReason,
2927
- totalUsage,
2928
- output: experimentalOutput,
2929
- ...(hasTerminalError ? { error: terminalError } : {}),
2930
- };
2931
3197
  }
2932
3198
  }
2933
3199
 
@@ -3393,3 +3659,45 @@ async function executeTool(
3393
3659
  isError: false,
3394
3660
  };
3395
3661
  }
3662
+
3663
+ function aggregateExecutionUsage(
3664
+ steps: StepResult<any, any>[],
3665
+ mode: 'generate' | 'stream',
3666
+ ): LanguageModelUsage {
3667
+ return mode === 'stream'
3668
+ ? aggregateUsage(steps)
3669
+ : steps.reduce(
3670
+ (usage, step) => addLanguageModelUsage(usage, step.usage),
3671
+ createNullLanguageModelUsage(),
3672
+ );
3673
+ }
3674
+
3675
+ function addApprovalRequestsToMessages(
3676
+ messages: ModelMessage[],
3677
+ requests: Array<{
3678
+ type: 'tool-approval-request';
3679
+ approvalId: string;
3680
+ toolCallId: string;
3681
+ signature?: string;
3682
+ }>,
3683
+ ): ModelMessage[] {
3684
+ if (requests.length === 0) return messages;
3685
+ const result = [...messages];
3686
+ for (let index = result.length - 1; index >= 0; index--) {
3687
+ const message = result[index];
3688
+ if (message.role === 'assistant') {
3689
+ result[index] = {
3690
+ ...message,
3691
+ content: [
3692
+ ...(typeof message.content === 'string'
3693
+ ? [{ type: 'text' as const, text: message.content }]
3694
+ : message.content),
3695
+ ...requests,
3696
+ ],
3697
+ };
3698
+ return result;
3699
+ }
3700
+ }
3701
+ result.unshift({ role: 'assistant', content: requests });
3702
+ return result;
3703
+ }