@ai-sdk/workflow 2.0.70 → 2.0.72

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.
@@ -51,6 +51,8 @@ import {
51
51
  convertToLanguageModelPrompt,
52
52
  mergeAbortSignals,
53
53
  mergeCallbacks,
54
+ mapWithConcurrency,
55
+ prepareToolCallConcurrency,
54
56
  signToolApproval,
55
57
  standardizePrompt,
56
58
  validateApprovedToolApprovals,
@@ -426,6 +428,7 @@ export interface PrepareCallOptions<
426
428
  tools: TTools;
427
429
  instructions?: Instructions;
428
430
  toolChoice?: ToolChoice<TTools>;
431
+ toolCallConcurrency?: number;
429
432
  stopWhen?:
430
433
  | StopCondition<NoInfer<TTools>, TRuntimeContext>
431
434
  | Array<StopCondition<NoInfer<TTools>, TRuntimeContext>>;
@@ -514,6 +517,13 @@ export type WorkflowAgentOptions<
514
517
  */
515
518
  toolChoice?: ToolChoice<TTools>;
516
519
 
520
+ /**
521
+ * Maximum number of local tool calls that may execute concurrently within a step.
522
+ * Set to `1` to execute calls sequentially in model order, including approved
523
+ * calls. By default, all local tool calls execute concurrently.
524
+ */
525
+ toolCallConcurrency?: number;
526
+
517
527
  /**
518
528
  * Optional telemetry configuration.
519
529
  */
@@ -578,6 +588,7 @@ export type WorkflowAgentOptions<
578
588
 
579
589
  /**
580
590
  * Default custom download function to use for URLs.
591
+ * Mark the function with `'use step'` when used in a workflow.
581
592
  *
582
593
  * Per-stream `experimental_download` values passed to `stream()` override this default.
583
594
  */
@@ -998,6 +1009,13 @@ export type WorkflowAgentCallOptions<
998
1009
  */
999
1010
  toolChoice?: ToolChoice<TTools>;
1000
1011
 
1012
+ /**
1013
+ * Maximum number of local tool calls that may execute concurrently within a step.
1014
+ * Set to `1` to execute calls sequentially in model order, including approved
1015
+ * calls. By default, all local tool calls execute concurrently.
1016
+ */
1017
+ toolCallConcurrency?: number;
1018
+
1001
1019
  /**
1002
1020
  * Limits the tools that are available for the model to call without
1003
1021
  * changing the tool call and result types in the result.
@@ -1074,6 +1092,7 @@ export type WorkflowAgentCallOptions<
1074
1092
  /**
1075
1093
  * Custom download function to use for URLs.
1076
1094
  * By default, files are downloaded if the model does not support the URL for the given media type.
1095
+ * Mark the function with `'use step'` when used in a workflow.
1077
1096
  */
1078
1097
  experimental_download?: DownloadFunction;
1079
1098
 
@@ -1538,6 +1557,7 @@ export class WorkflowAgent<
1538
1557
  private instructions?: Instructions;
1539
1558
  private generationSettings: GenerationSettings;
1540
1559
  private toolChoice?: ToolChoice<TBaseTools>;
1560
+ private toolCallConcurrency?: number;
1541
1561
  private telemetry?: TelemetryOptions<TRuntimeContext, TBaseTools>;
1542
1562
  private runtimeContext?: TRuntimeContext;
1543
1563
  private toolsContext?: InferToolSetContext<TBaseTools>;
@@ -1586,6 +1606,7 @@ export class WorkflowAgent<
1586
1606
  // `instructions` takes precedence over deprecated `system`
1587
1607
  this.instructions = options.instructions ?? options.system;
1588
1608
  this.toolChoice = options.toolChoice;
1609
+ this.toolCallConcurrency = options.toolCallConcurrency;
1589
1610
  this.telemetry = options.telemetry;
1590
1611
  this.runtimeContext = options.runtimeContext;
1591
1612
  this.toolsContext = options.toolsContext;
@@ -1725,6 +1746,8 @@ export class WorkflowAgent<
1725
1746
  Context | undefined
1726
1747
  >;
1727
1748
  let effectiveToolChoiceFromPrepare = options.toolChoice ?? this.toolChoice;
1749
+ let effectiveToolCallConcurrency =
1750
+ options.toolCallConcurrency ?? this.toolCallConcurrency;
1728
1751
  let effectiveStopWhenFromPrepare =
1729
1752
  options.stopWhen ??
1730
1753
  this.stopWhen ??
@@ -1752,6 +1775,7 @@ export class WorkflowAgent<
1752
1775
  tools: this.tools,
1753
1776
  instructions: effectiveInstructions,
1754
1777
  toolChoice: effectiveToolChoiceFromPrepare as ToolChoice<TBaseTools>,
1778
+ toolCallConcurrency: effectiveToolCallConcurrency,
1755
1779
  stopWhen: effectiveStopWhenFromPrepare,
1756
1780
  activeTools: effectiveActiveToolsFromPrepare,
1757
1781
  experimental_download: effectiveDownloadFromPrepare,
@@ -1779,6 +1803,8 @@ export class WorkflowAgent<
1779
1803
  if (prepared.toolChoice !== undefined)
1780
1804
  effectiveToolChoiceFromPrepare =
1781
1805
  prepared.toolChoice as ToolChoice<TBaseTools>;
1806
+ if (prepared.toolCallConcurrency !== undefined)
1807
+ effectiveToolCallConcurrency = prepared.toolCallConcurrency;
1782
1808
  if (prepared.stopWhen !== undefined)
1783
1809
  effectiveStopWhenFromPrepare = prepared.stopWhen;
1784
1810
  if (prepared.activeTools !== undefined)
@@ -1816,6 +1842,9 @@ export class WorkflowAgent<
1816
1842
  effectiveGenerationSettings.providerOptions = prepared.providerOptions;
1817
1843
  }
1818
1844
 
1845
+ const toolCallConcurrency = prepareToolCallConcurrency(
1846
+ options.toolCallConcurrency ?? effectiveToolCallConcurrency,
1847
+ );
1819
1848
  const effectiveTelemetry = effectiveTelemetryFromPrepare;
1820
1849
  const telemetryDispatcher = createRestrictedTelemetryDispatcher<
1821
1850
  any,
@@ -1834,7 +1863,8 @@ export class WorkflowAgent<
1834
1863
  ? { prompt: effectivePrompt }
1835
1864
  : { messages: effectiveMessages! }),
1836
1865
  } as Prompt);
1837
- const download = effectiveDownloadFromPrepare;
1866
+ const workflowDownload: DownloadFunction = async requests =>
1867
+ requests.map(() => null);
1838
1868
  const sandbox = options.experimental_sandbox ?? this.experimentalSandbox;
1839
1869
  // Model steps enforce the absolute deadline below. Avoid creating a native
1840
1870
  // timeout signal in the workflow VM, where timer APIs are unavailable.
@@ -1866,13 +1896,15 @@ export class WorkflowAgent<
1866
1896
  effectiveRuntimeContext,
1867
1897
  effectiveToolsContext,
1868
1898
  effectiveToolChoiceFromPrepare,
1899
+ toolCallConcurrency,
1869
1900
  effectiveStopWhenFromPrepare,
1870
1901
  effectiveActiveToolsFromPrepare,
1871
1902
  effectiveToolApprovalSecret,
1872
1903
  effectiveTelemetry,
1873
1904
  telemetryDispatcher,
1874
1905
  prompt,
1875
- download,
1906
+ download: effectiveDownloadFromPrepare,
1907
+ workflowDownload,
1876
1908
  sandbox,
1877
1909
  effectiveAbortSignal,
1878
1910
  timeoutAt,
@@ -1902,6 +1934,7 @@ export class WorkflowAgent<
1902
1934
  effectiveRuntimeContext,
1903
1935
  effectiveToolsContext,
1904
1936
  effectiveToolChoiceFromPrepare,
1937
+ toolCallConcurrency,
1905
1938
  effectiveStopWhenFromPrepare,
1906
1939
  effectiveActiveToolsFromPrepare,
1907
1940
  effectiveToolApprovalSecret,
@@ -1909,6 +1942,7 @@ export class WorkflowAgent<
1909
1942
  telemetryDispatcher,
1910
1943
  prompt,
1911
1944
  download,
1945
+ workflowDownload,
1912
1946
  sandbox,
1913
1947
  effectiveAbortSignal,
1914
1948
  timeoutAt,
@@ -1980,52 +2014,56 @@ export class WorkflowAgent<
1980
2014
  output: unknown;
1981
2015
  }> = [];
1982
2016
 
1983
- // Execute approved tools
1984
- for (const approval of approvedToolApprovals) {
1985
- // Provider-executed approvals are forwarded to the provider via the
1986
- // preserved approval response below, not executed locally.
1987
- if (approval.providerExecuted) {
1988
- continue;
1989
- }
1990
- const tool = (this.tools as ToolSet)[approval.toolName];
1991
- if (tool && typeof tool.execute === 'function') {
1992
- if (!tool.needsApproval) {
1993
- const reason = `Tool "${approval.toolName}" does not require approval`;
1994
- toolResultContent.push({
1995
- type: 'tool-result' as const,
1996
- toolCallId: approval.toolCallId,
1997
- toolName: approval.toolName,
1998
- output: await createLanguageModelToolResultOutput({
2017
+ // Execute approved calls in model order with the same limit as new calls.
2018
+ const approvedResults = await mapWithConcurrency({
2019
+ items: approvedToolApprovals.filter(
2020
+ approval => !approval.providerExecuted,
2021
+ ),
2022
+ concurrency: toolCallConcurrency,
2023
+ abortSignal: effectiveAbortSignal,
2024
+ execute: async approval => {
2025
+ const tool = (this.tools as ToolSet)[approval.toolName];
2026
+ if (tool && typeof tool.execute === 'function') {
2027
+ if (!tool.needsApproval) {
2028
+ const reason = `Tool "${approval.toolName}" does not require approval`;
2029
+ return {
2030
+ modelResult: {
2031
+ type: 'tool-result' as const,
2032
+ toolCallId: approval.toolCallId,
2033
+ toolName: approval.toolName,
2034
+ output: await createLanguageModelToolResultOutput({
2035
+ toolCallId: approval.toolCallId,
2036
+ toolName: approval.toolName,
2037
+ input: approval.input,
2038
+ output: reason,
2039
+ tool,
2040
+ errorMode: 'text',
2041
+ supportedUrls: {},
2042
+ download: workflowDownload,
2043
+ }),
2044
+ },
2045
+ };
2046
+ }
2047
+
2048
+ // Re-validate through the shared core implementation: input schema,
2049
+ // approval signature (when configured), and approval policy.
2050
+ // Convert invalid input, denial, or signature errors to a tool error
2051
+ // result so the agent loop can continue gracefully.
2052
+ let revalidationReason: string | undefined;
2053
+ try {
2054
+ await validateWorkflowToolApprovalSignature({
2055
+ secret: effectiveToolApprovalSecret,
2056
+ approvalId: approval.collected.approvalRequest.approvalId,
1999
2057
  toolCallId: approval.toolCallId,
2000
2058
  toolName: approval.toolName,
2001
2059
  input: approval.input,
2002
- output: reason,
2003
- tool,
2004
- errorMode: 'text',
2005
- supportedUrls: {},
2006
- download,
2007
- }),
2008
- });
2009
- continue;
2010
- }
2011
-
2012
- // Re-validate through the shared core implementation: input schema,
2013
- // approval signature (when configured), and approval policy.
2014
- // Convert invalid input, denial, or signature errors to a tool error
2015
- // result so the agent loop can continue gracefully.
2016
- let revalidationReason: string | undefined;
2017
- try {
2018
- await validateWorkflowToolApprovalSignature({
2019
- secret: effectiveToolApprovalSecret,
2020
- approvalId: approval.collected.approvalRequest.approvalId,
2021
- toolCallId: approval.toolCallId,
2022
- toolName: approval.toolName,
2023
- input: approval.input,
2024
- signature: approval.collected.approvalRequest.signature,
2025
- });
2060
+ signature: approval.collected.approvalRequest.signature,
2061
+ });
2026
2062
 
2027
- const { deniedToolApprovals: policyDenied, invalidToolApprovals } =
2028
- await validateApprovedToolApprovals({
2063
+ const {
2064
+ deniedToolApprovals: policyDenied,
2065
+ invalidToolApprovals,
2066
+ } = await validateApprovedToolApprovals({
2029
2067
  approvedToolApprovals: [approval.collected],
2030
2068
  tools: this.tools as ToolSet,
2031
2069
  toolApproval: undefined,
@@ -2035,58 +2073,67 @@ export class WorkflowAgent<
2035
2073
  runtimeContext: effectiveRuntimeContext,
2036
2074
  });
2037
2075
 
2038
- if (invalidToolApprovals.length > 0) {
2039
- revalidationReason = getErrorMessage(
2040
- invalidToolApprovals[0].error,
2041
- );
2042
- } else if (policyDenied.length > 0) {
2043
- revalidationReason =
2044
- policyDenied[0].approvalResponse.reason ??
2045
- 'Tool approval denied';
2076
+ if (invalidToolApprovals.length > 0) {
2077
+ revalidationReason = getErrorMessage(
2078
+ invalidToolApprovals[0].error,
2079
+ );
2080
+ } else if (policyDenied.length > 0) {
2081
+ revalidationReason =
2082
+ policyDenied[0].approvalResponse.reason ??
2083
+ 'Tool approval denied';
2084
+ }
2085
+ } catch (error) {
2086
+ revalidationReason = getErrorMessage(error);
2046
2087
  }
2047
- } catch (error) {
2048
- revalidationReason = getErrorMessage(error);
2049
- }
2050
2088
 
2051
- if (revalidationReason != null) {
2052
- toolResultContent.push({
2053
- type: 'tool-result' as const,
2054
- toolCallId: approval.toolCallId,
2055
- toolName: approval.toolName,
2056
- output: await createLanguageModelToolResultOutput({
2089
+ if (revalidationReason != null) {
2090
+ return {
2091
+ modelResult: {
2092
+ type: 'tool-result' as const,
2093
+ toolCallId: approval.toolCallId,
2094
+ toolName: approval.toolName,
2095
+ output: await createLanguageModelToolResultOutput({
2096
+ toolCallId: approval.toolCallId,
2097
+ toolName: approval.toolName,
2098
+ input: approval.input,
2099
+ output: revalidationReason,
2100
+ tool,
2101
+ errorMode: 'text',
2102
+ supportedUrls: {},
2103
+ download: workflowDownload,
2104
+ }),
2105
+ },
2106
+ };
2107
+ }
2108
+
2109
+ const result = await executeToolWithCallbacks(
2110
+ {
2057
2111
  toolCallId: approval.toolCallId,
2058
2112
  toolName: approval.toolName,
2059
2113
  input: approval.input,
2060
- output: revalidationReason,
2061
- tool,
2062
- errorMode: 'text',
2063
- supportedUrls: {},
2064
- download,
2065
- }),
2066
- });
2067
- continue;
2114
+ },
2115
+ this.tools as ToolSet,
2116
+ prompt.messages as unknown as LanguageModelV4Prompt,
2117
+ effectiveToolsContext,
2118
+ 0,
2119
+ sandbox,
2120
+ );
2121
+ return {
2122
+ modelResult: result.modelResult,
2123
+ rawResult: {
2124
+ toolCallId: approval.toolCallId,
2125
+ toolName: approval.toolName,
2126
+ input: approval.input,
2127
+ output: result.rawOutput,
2128
+ },
2129
+ };
2068
2130
  }
2069
-
2070
- const result = await executeToolWithCallbacks(
2071
- {
2072
- toolCallId: approval.toolCallId,
2073
- toolName: approval.toolName,
2074
- input: approval.input,
2075
- },
2076
- this.tools as ToolSet,
2077
- prompt.messages as unknown as LanguageModelV4Prompt,
2078
- effectiveToolsContext,
2079
- 0,
2080
- sandbox,
2081
- );
2082
- toolResultContent.push(result.modelResult);
2083
- approvedRawResults.push({
2084
- toolCallId: approval.toolCallId,
2085
- toolName: approval.toolName,
2086
- input: approval.input,
2087
- output: result.rawOutput,
2088
- });
2089
- }
2131
+ },
2132
+ });
2133
+ for (const result of approvedResults) {
2134
+ if (result == null) continue;
2135
+ toolResultContent.push(result.modelResult);
2136
+ if (result.rawResult != null) approvedRawResults.push(result.rawResult);
2090
2137
  }
2091
2138
 
2092
2139
  // Create denial results for denied tools
@@ -2183,7 +2230,7 @@ export class WorkflowAgent<
2183
2230
  const modelPrompt = await convertToLanguageModelPrompt({
2184
2231
  prompt,
2185
2232
  supportedUrls: {},
2186
- download,
2233
+ download: workflowDownload,
2187
2234
  });
2188
2235
 
2189
2236
  // Merge generation settings: constructor defaults < prepareCall < stream options
@@ -2368,7 +2415,7 @@ export class WorkflowAgent<
2368
2415
  messages,
2369
2416
  resolvedContext,
2370
2417
  effectiveAbortSignal,
2371
- download,
2418
+ workflowDownload,
2372
2419
  stepSandbox,
2373
2420
  );
2374
2421
  result =
@@ -2546,6 +2593,7 @@ export class WorkflowAgent<
2546
2593
  repairToolCall: (options.repairToolCall ??
2547
2594
  options.experimental_repairToolCall ??
2548
2595
  this.repairToolCall) as ToolCallRepairFunction<ToolSet> | undefined,
2596
+ download,
2549
2597
  responseFormat: await (options.output ?? this.output)?.responseFormat,
2550
2598
  experimental_transform: options.experimental_transform as
2551
2599
  | StreamTextTransform<ToolSet>
@@ -2705,19 +2753,20 @@ export class WorkflowAgent<
2705
2753
  providerApprovalRequests.length > 0
2706
2754
  ) {
2707
2755
  // Execute any executable tools that were also called in this step
2708
- const executableResults = await Promise.all(
2709
- executableToolCalls.map(
2710
- (toolCall): Promise<WorkflowToolExecutionResult> =>
2711
- executeToolWithCallbacks(
2712
- toolCall,
2713
- stepTools,
2714
- iterMessages,
2715
- toolsContext,
2716
- currentStepNumber,
2717
- toolExecutionSandbox,
2718
- ),
2719
- ),
2720
- );
2756
+ const executableResults = await mapWithConcurrency({
2757
+ items: executableToolCalls,
2758
+ concurrency: toolCallConcurrency,
2759
+ abortSignal: effectiveAbortSignal,
2760
+ execute: toolCall =>
2761
+ executeToolWithCallbacks(
2762
+ toolCall,
2763
+ stepTools,
2764
+ iterMessages,
2765
+ toolsContext,
2766
+ currentStepNumber,
2767
+ toolExecutionSandbox,
2768
+ ),
2769
+ });
2721
2770
 
2722
2771
  // Collect provider tool results
2723
2772
  const providerResultEntries = await Promise.all(
@@ -2735,7 +2784,7 @@ export class WorkflowAgent<
2735
2784
  toolCall,
2736
2785
  capturedProviderToolResults,
2737
2786
  stepTools,
2738
- download,
2787
+ workflowDownload,
2739
2788
  ),
2740
2789
  })),
2741
2790
  );
@@ -2920,19 +2969,20 @@ export class WorkflowAgent<
2920
2969
  }
2921
2970
 
2922
2971
  // Execute client tools (all have execute functions at this point)
2923
- const clientToolResults = await Promise.all(
2924
- nonProviderToolCalls.map(
2925
- (toolCall): Promise<WorkflowToolExecutionResult> =>
2926
- executeToolWithCallbacks(
2927
- toolCall,
2928
- stepTools,
2929
- iterMessages,
2930
- toolsContext,
2931
- currentStepNumber,
2932
- toolExecutionSandbox,
2933
- ),
2934
- ),
2935
- );
2972
+ const clientToolResults = await mapWithConcurrency({
2973
+ items: nonProviderToolCalls,
2974
+ concurrency: toolCallConcurrency,
2975
+ abortSignal: effectiveAbortSignal,
2976
+ execute: toolCall =>
2977
+ executeToolWithCallbacks(
2978
+ toolCall,
2979
+ stepTools,
2980
+ iterMessages,
2981
+ toolsContext,
2982
+ currentStepNumber,
2983
+ toolExecutionSandbox,
2984
+ ),
2985
+ });
2936
2986
 
2937
2987
  // For provider-executed tools, use the results from the stream
2938
2988
  const providerToolResultEntries = await Promise.all(
@@ -2942,7 +2992,7 @@ export class WorkflowAgent<
2942
2992
  toolCall,
2943
2993
  capturedProviderToolResults,
2944
2994
  stepTools,
2945
- download,
2995
+ workflowDownload,
2946
2996
  ),
2947
2997
  })),
2948
2998
  );