@ai-sdk/workflow 2.0.71 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/workflow",
3
- "version": "2.0.71",
3
+ "version": "2.0.72",
4
4
  "type": "module",
5
5
  "description": "WorkflowAgent for building AI agents with AI SDK",
6
6
  "license": "Apache-2.0",
@@ -38,11 +38,11 @@
38
38
  "dependencies": {
39
39
  "@ai-sdk/provider": "4.0.27",
40
40
  "@ai-sdk/provider-utils": "5.0.59",
41
- "ai": "7.0.139",
41
+ "ai": "7.0.140",
42
42
  "ajv": "^8.20.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@ai-sdk/otel": "1.0.139",
45
+ "@ai-sdk/otel": "1.0.140",
46
46
  "@opentelemetry/api": "1.9.1",
47
47
  "@opentelemetry/sdk-trace-base": "2.7.1",
48
48
  "@types/node": "22.19.19",
@@ -0,0 +1,66 @@
1
+ import { tool, type ModelMessage } from 'ai';
2
+ import { createHook } from 'workflow';
3
+ import { z } from 'zod/v4';
4
+ import { mockTextModel } from '../providers/mock.js';
5
+ import { WorkflowAgent } from '../workflow-agent.js';
6
+
7
+ async function completeAction(id: string) {
8
+ 'use step';
9
+ return id;
10
+ }
11
+
12
+ export async function resumeSequentialApprovals(mode: 'generate' | 'stream') {
13
+ 'use workflow';
14
+ const events: string[] = [];
15
+ const agent = new WorkflowAgent({
16
+ model: mockTextModel('Done.'),
17
+ toolCallConcurrency: 1,
18
+ tools: {
19
+ action: tool({
20
+ inputSchema: z.object({ id: z.string() }),
21
+ needsApproval: true,
22
+ execute: async ({ id }) => {
23
+ events.push(`start:${id}`);
24
+ if (id === 'first') {
25
+ using hook = createHook<string>();
26
+ await hook;
27
+ }
28
+ const result = await completeAction(id);
29
+ events.push(`end:${id}`);
30
+ return result;
31
+ },
32
+ }),
33
+ },
34
+ });
35
+ const ids = ['first', 'second'];
36
+ const messages: ModelMessage[] = [
37
+ { role: 'user', content: 'Run the tools.' },
38
+ {
39
+ role: 'assistant',
40
+ content: ids.flatMap(id => [
41
+ {
42
+ type: 'tool-call' as const,
43
+ toolCallId: id,
44
+ toolName: 'action',
45
+ input: { id },
46
+ },
47
+ {
48
+ type: 'tool-approval-request' as const,
49
+ toolCallId: id,
50
+ approvalId: `approval-${id}`,
51
+ },
52
+ ]),
53
+ },
54
+ {
55
+ role: 'tool',
56
+ content: [...ids].reverse().map(id => ({
57
+ type: 'tool-approval-response' as const,
58
+ approvalId: `approval-${id}`,
59
+ approved: true,
60
+ })),
61
+ },
62
+ ];
63
+ if (mode === 'generate') await agent.generate({ messages });
64
+ else await agent.stream({ messages });
65
+ return events;
66
+ }
@@ -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
  */
@@ -999,6 +1009,13 @@ export type WorkflowAgentCallOptions<
999
1009
  */
1000
1010
  toolChoice?: ToolChoice<TTools>;
1001
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
+
1002
1019
  /**
1003
1020
  * Limits the tools that are available for the model to call without
1004
1021
  * changing the tool call and result types in the result.
@@ -1540,6 +1557,7 @@ export class WorkflowAgent<
1540
1557
  private instructions?: Instructions;
1541
1558
  private generationSettings: GenerationSettings;
1542
1559
  private toolChoice?: ToolChoice<TBaseTools>;
1560
+ private toolCallConcurrency?: number;
1543
1561
  private telemetry?: TelemetryOptions<TRuntimeContext, TBaseTools>;
1544
1562
  private runtimeContext?: TRuntimeContext;
1545
1563
  private toolsContext?: InferToolSetContext<TBaseTools>;
@@ -1588,6 +1606,7 @@ export class WorkflowAgent<
1588
1606
  // `instructions` takes precedence over deprecated `system`
1589
1607
  this.instructions = options.instructions ?? options.system;
1590
1608
  this.toolChoice = options.toolChoice;
1609
+ this.toolCallConcurrency = options.toolCallConcurrency;
1591
1610
  this.telemetry = options.telemetry;
1592
1611
  this.runtimeContext = options.runtimeContext;
1593
1612
  this.toolsContext = options.toolsContext;
@@ -1727,6 +1746,8 @@ export class WorkflowAgent<
1727
1746
  Context | undefined
1728
1747
  >;
1729
1748
  let effectiveToolChoiceFromPrepare = options.toolChoice ?? this.toolChoice;
1749
+ let effectiveToolCallConcurrency =
1750
+ options.toolCallConcurrency ?? this.toolCallConcurrency;
1730
1751
  let effectiveStopWhenFromPrepare =
1731
1752
  options.stopWhen ??
1732
1753
  this.stopWhen ??
@@ -1754,6 +1775,7 @@ export class WorkflowAgent<
1754
1775
  tools: this.tools,
1755
1776
  instructions: effectiveInstructions,
1756
1777
  toolChoice: effectiveToolChoiceFromPrepare as ToolChoice<TBaseTools>,
1778
+ toolCallConcurrency: effectiveToolCallConcurrency,
1757
1779
  stopWhen: effectiveStopWhenFromPrepare,
1758
1780
  activeTools: effectiveActiveToolsFromPrepare,
1759
1781
  experimental_download: effectiveDownloadFromPrepare,
@@ -1781,6 +1803,8 @@ export class WorkflowAgent<
1781
1803
  if (prepared.toolChoice !== undefined)
1782
1804
  effectiveToolChoiceFromPrepare =
1783
1805
  prepared.toolChoice as ToolChoice<TBaseTools>;
1806
+ if (prepared.toolCallConcurrency !== undefined)
1807
+ effectiveToolCallConcurrency = prepared.toolCallConcurrency;
1784
1808
  if (prepared.stopWhen !== undefined)
1785
1809
  effectiveStopWhenFromPrepare = prepared.stopWhen;
1786
1810
  if (prepared.activeTools !== undefined)
@@ -1818,6 +1842,9 @@ export class WorkflowAgent<
1818
1842
  effectiveGenerationSettings.providerOptions = prepared.providerOptions;
1819
1843
  }
1820
1844
 
1845
+ const toolCallConcurrency = prepareToolCallConcurrency(
1846
+ options.toolCallConcurrency ?? effectiveToolCallConcurrency,
1847
+ );
1821
1848
  const effectiveTelemetry = effectiveTelemetryFromPrepare;
1822
1849
  const telemetryDispatcher = createRestrictedTelemetryDispatcher<
1823
1850
  any,
@@ -1869,6 +1896,7 @@ export class WorkflowAgent<
1869
1896
  effectiveRuntimeContext,
1870
1897
  effectiveToolsContext,
1871
1898
  effectiveToolChoiceFromPrepare,
1899
+ toolCallConcurrency,
1872
1900
  effectiveStopWhenFromPrepare,
1873
1901
  effectiveActiveToolsFromPrepare,
1874
1902
  effectiveToolApprovalSecret,
@@ -1906,6 +1934,7 @@ export class WorkflowAgent<
1906
1934
  effectiveRuntimeContext,
1907
1935
  effectiveToolsContext,
1908
1936
  effectiveToolChoiceFromPrepare,
1937
+ toolCallConcurrency,
1909
1938
  effectiveStopWhenFromPrepare,
1910
1939
  effectiveActiveToolsFromPrepare,
1911
1940
  effectiveToolApprovalSecret,
@@ -1985,52 +2014,56 @@ export class WorkflowAgent<
1985
2014
  output: unknown;
1986
2015
  }> = [];
1987
2016
 
1988
- // Execute approved tools
1989
- for (const approval of approvedToolApprovals) {
1990
- // Provider-executed approvals are forwarded to the provider via the
1991
- // preserved approval response below, not executed locally.
1992
- if (approval.providerExecuted) {
1993
- continue;
1994
- }
1995
- const tool = (this.tools as ToolSet)[approval.toolName];
1996
- if (tool && typeof tool.execute === 'function') {
1997
- if (!tool.needsApproval) {
1998
- const reason = `Tool "${approval.toolName}" does not require approval`;
1999
- toolResultContent.push({
2000
- type: 'tool-result' as const,
2001
- toolCallId: approval.toolCallId,
2002
- toolName: approval.toolName,
2003
- 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,
2004
2057
  toolCallId: approval.toolCallId,
2005
2058
  toolName: approval.toolName,
2006
2059
  input: approval.input,
2007
- output: reason,
2008
- tool,
2009
- errorMode: 'text',
2010
- supportedUrls: {},
2011
- download: workflowDownload,
2012
- }),
2013
- });
2014
- continue;
2015
- }
2016
-
2017
- // Re-validate through the shared core implementation: input schema,
2018
- // approval signature (when configured), and approval policy.
2019
- // Convert invalid input, denial, or signature errors to a tool error
2020
- // result so the agent loop can continue gracefully.
2021
- let revalidationReason: string | undefined;
2022
- try {
2023
- await validateWorkflowToolApprovalSignature({
2024
- secret: effectiveToolApprovalSecret,
2025
- approvalId: approval.collected.approvalRequest.approvalId,
2026
- toolCallId: approval.toolCallId,
2027
- toolName: approval.toolName,
2028
- input: approval.input,
2029
- signature: approval.collected.approvalRequest.signature,
2030
- });
2060
+ signature: approval.collected.approvalRequest.signature,
2061
+ });
2031
2062
 
2032
- const { deniedToolApprovals: policyDenied, invalidToolApprovals } =
2033
- await validateApprovedToolApprovals({
2063
+ const {
2064
+ deniedToolApprovals: policyDenied,
2065
+ invalidToolApprovals,
2066
+ } = await validateApprovedToolApprovals({
2034
2067
  approvedToolApprovals: [approval.collected],
2035
2068
  tools: this.tools as ToolSet,
2036
2069
  toolApproval: undefined,
@@ -2040,58 +2073,67 @@ export class WorkflowAgent<
2040
2073
  runtimeContext: effectiveRuntimeContext,
2041
2074
  });
2042
2075
 
2043
- if (invalidToolApprovals.length > 0) {
2044
- revalidationReason = getErrorMessage(
2045
- invalidToolApprovals[0].error,
2046
- );
2047
- } else if (policyDenied.length > 0) {
2048
- revalidationReason =
2049
- policyDenied[0].approvalResponse.reason ??
2050
- '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);
2051
2087
  }
2052
- } catch (error) {
2053
- revalidationReason = getErrorMessage(error);
2054
- }
2055
2088
 
2056
- if (revalidationReason != null) {
2057
- toolResultContent.push({
2058
- type: 'tool-result' as const,
2059
- toolCallId: approval.toolCallId,
2060
- toolName: approval.toolName,
2061
- 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
+ {
2062
2111
  toolCallId: approval.toolCallId,
2063
2112
  toolName: approval.toolName,
2064
2113
  input: approval.input,
2065
- output: revalidationReason,
2066
- tool,
2067
- errorMode: 'text',
2068
- supportedUrls: {},
2069
- download: workflowDownload,
2070
- }),
2071
- });
2072
- 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
+ };
2073
2130
  }
2074
-
2075
- const result = await executeToolWithCallbacks(
2076
- {
2077
- toolCallId: approval.toolCallId,
2078
- toolName: approval.toolName,
2079
- input: approval.input,
2080
- },
2081
- this.tools as ToolSet,
2082
- prompt.messages as unknown as LanguageModelV4Prompt,
2083
- effectiveToolsContext,
2084
- 0,
2085
- sandbox,
2086
- );
2087
- toolResultContent.push(result.modelResult);
2088
- approvedRawResults.push({
2089
- toolCallId: approval.toolCallId,
2090
- toolName: approval.toolName,
2091
- input: approval.input,
2092
- output: result.rawOutput,
2093
- });
2094
- }
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);
2095
2137
  }
2096
2138
 
2097
2139
  // Create denial results for denied tools
@@ -2711,19 +2753,20 @@ export class WorkflowAgent<
2711
2753
  providerApprovalRequests.length > 0
2712
2754
  ) {
2713
2755
  // Execute any executable tools that were also called in this step
2714
- const executableResults = await Promise.all(
2715
- executableToolCalls.map(
2716
- (toolCall): Promise<WorkflowToolExecutionResult> =>
2717
- executeToolWithCallbacks(
2718
- toolCall,
2719
- stepTools,
2720
- iterMessages,
2721
- toolsContext,
2722
- currentStepNumber,
2723
- toolExecutionSandbox,
2724
- ),
2725
- ),
2726
- );
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
+ });
2727
2770
 
2728
2771
  // Collect provider tool results
2729
2772
  const providerResultEntries = await Promise.all(
@@ -2926,19 +2969,20 @@ export class WorkflowAgent<
2926
2969
  }
2927
2970
 
2928
2971
  // Execute client tools (all have execute functions at this point)
2929
- const clientToolResults = await Promise.all(
2930
- nonProviderToolCalls.map(
2931
- (toolCall): Promise<WorkflowToolExecutionResult> =>
2932
- executeToolWithCallbacks(
2933
- toolCall,
2934
- stepTools,
2935
- iterMessages,
2936
- toolsContext,
2937
- currentStepNumber,
2938
- toolExecutionSandbox,
2939
- ),
2940
- ),
2941
- );
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
+ });
2942
2986
 
2943
2987
  // For provider-executed tools, use the results from the stream
2944
2988
  const providerToolResultEntries = await Promise.all(