@ai-sdk/workflow 1.0.0-beta.103 → 1.0.0-beta.105
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/CHANGELOG.md +14 -0
- package/dist/index.d.mts +22 -1
- package/dist/index.mjs +81 -54
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/stream-text-iterator.ts +12 -0
- package/src/test/agent-e2e-workflows.ts +86 -0
- package/src/test/test-sandbox.ts +26 -0
- package/src/workflow-agent.ts +54 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/workflow",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.105",
|
|
4
4
|
"description": "WorkflowAgent for building AI agents with AI SDK",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"ajv": "^8.20.0",
|
|
30
30
|
"@ai-sdk/provider": "4.0.0-beta.20",
|
|
31
31
|
"@ai-sdk/provider-utils": "5.0.0-beta.50",
|
|
32
|
-
"ai": "7.0.0-beta.
|
|
32
|
+
"ai": "7.0.0-beta.185"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "22.19.19",
|
|
@@ -7,6 +7,7 @@ import type { Context } from '@ai-sdk/provider-utils';
|
|
|
7
7
|
import {
|
|
8
8
|
experimental_filterActiveTools as filterActiveTools,
|
|
9
9
|
type Experimental_LanguageModelStreamPart as ModelCallStreamPart,
|
|
10
|
+
type Experimental_SandboxSession as SandboxSession,
|
|
10
11
|
type LanguageModel,
|
|
11
12
|
type ModelMessage,
|
|
12
13
|
type StepResult,
|
|
@@ -52,6 +53,8 @@ export interface StreamTextIteratorYieldValue {
|
|
|
52
53
|
toolsContext?: Record<string, Context | undefined>;
|
|
53
54
|
/** Provider-executed tool results (keyed by tool call ID) */
|
|
54
55
|
providerExecutedToolResults?: Map<string, ProviderExecutedToolResult>;
|
|
56
|
+
/** The sandbox selected for the current step. */
|
|
57
|
+
experimental_sandbox?: SandboxSession;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
// This runs in the workflow context
|
|
@@ -74,6 +77,7 @@ export async function* streamTextIterator({
|
|
|
74
77
|
includeRawChunks = false,
|
|
75
78
|
repairToolCall,
|
|
76
79
|
responseFormat,
|
|
80
|
+
experimental_sandbox: sandbox,
|
|
77
81
|
}: {
|
|
78
82
|
prompt: LanguageModelV4Prompt;
|
|
79
83
|
tools: ToolSet;
|
|
@@ -94,6 +98,7 @@ export async function* streamTextIterator({
|
|
|
94
98
|
includeRawChunks?: boolean;
|
|
95
99
|
repairToolCall?: ToolCallRepairFunction<ToolSet>;
|
|
96
100
|
responseFormat?: LanguageModelV4CallOptions['responseFormat'];
|
|
101
|
+
experimental_sandbox?: SandboxSession;
|
|
97
102
|
}): AsyncGenerator<
|
|
98
103
|
StreamTextIteratorYieldValue,
|
|
99
104
|
LanguageModelV4Prompt,
|
|
@@ -135,6 +140,8 @@ export async function* streamTextIterator({
|
|
|
135
140
|
break;
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
let stepSandbox = sandbox;
|
|
144
|
+
|
|
138
145
|
// Call prepareStep callback before each step if provided
|
|
139
146
|
if (prepareStep) {
|
|
140
147
|
const prepareResult = await prepareStep({
|
|
@@ -144,8 +151,11 @@ export async function* streamTextIterator({
|
|
|
144
151
|
messages: conversationPrompt,
|
|
145
152
|
runtimeContext: currentRuntimeContext,
|
|
146
153
|
toolsContext: currentToolsContext as never,
|
|
154
|
+
experimental_sandbox: sandbox,
|
|
147
155
|
});
|
|
148
156
|
|
|
157
|
+
stepSandbox = prepareResult?.experimental_sandbox ?? sandbox;
|
|
158
|
+
|
|
149
159
|
// Apply any overrides from prepareStep
|
|
150
160
|
if (prepareResult?.model !== undefined) {
|
|
151
161
|
currentModel = prepareResult.model;
|
|
@@ -398,6 +408,7 @@ export async function* streamTextIterator({
|
|
|
398
408
|
step,
|
|
399
409
|
runtimeContext: currentRuntimeContext,
|
|
400
410
|
toolsContext: currentToolsContext,
|
|
411
|
+
experimental_sandbox: stepSandbox,
|
|
401
412
|
providerExecutedToolResults,
|
|
402
413
|
};
|
|
403
414
|
|
|
@@ -473,6 +484,7 @@ export async function* streamTextIterator({
|
|
|
473
484
|
step: lastStep,
|
|
474
485
|
runtimeContext: currentRuntimeContext,
|
|
475
486
|
toolsContext: currentToolsContext,
|
|
487
|
+
experimental_sandbox: sandbox,
|
|
476
488
|
};
|
|
477
489
|
}
|
|
478
490
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { tool } from 'ai';
|
|
5
5
|
import { WorkflowAgent } from '../workflow-agent.js';
|
|
6
6
|
import { mockTextModel, mockSequenceModel } from '../providers/mock.js';
|
|
7
|
+
import { createTestSandbox } from './test-sandbox.js';
|
|
7
8
|
import { FatalError, getWritable } from 'workflow';
|
|
8
9
|
import { z } from 'zod';
|
|
9
10
|
|
|
@@ -585,3 +586,88 @@ export async function agentRuntimeAndToolsContextE2e() {
|
|
|
585
586
|
onFinishToolsContext,
|
|
586
587
|
};
|
|
587
588
|
}
|
|
589
|
+
|
|
590
|
+
export async function agentSandboxE2e() {
|
|
591
|
+
'use workflow';
|
|
592
|
+
|
|
593
|
+
let constructorSandboxRanCommand = 'not-run';
|
|
594
|
+
let stepSandboxRanCommand = 'not-run';
|
|
595
|
+
let firstPrepareStepSawConstructorSandbox = false;
|
|
596
|
+
let secondPrepareStepSawConstructorSandbox = false;
|
|
597
|
+
let prepareStepSawStepSandbox = false;
|
|
598
|
+
|
|
599
|
+
// A live sandbox session passed through `experimental_sandbox`. The tool
|
|
600
|
+
// `execute` is inline (not a `'use step'`) so the handle never crosses a
|
|
601
|
+
// step boundary — matching the single-process use of `experimental_sandbox`.
|
|
602
|
+
const constructorSandbox = createTestSandbox({
|
|
603
|
+
run: async ({ command }) => {
|
|
604
|
+
constructorSandboxRanCommand = command;
|
|
605
|
+
return {
|
|
606
|
+
exitCode: 0,
|
|
607
|
+
stdout: `constructor ran: ${command}`,
|
|
608
|
+
stderr: '',
|
|
609
|
+
};
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
const stepSandbox = createTestSandbox({
|
|
613
|
+
run: async ({ command }) => {
|
|
614
|
+
stepSandboxRanCommand = command;
|
|
615
|
+
return { exitCode: 0, stdout: `ran: ${command}`, stderr: '' };
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const agent = new WorkflowAgent({
|
|
620
|
+
model: mockSequenceModel([
|
|
621
|
+
{
|
|
622
|
+
type: 'tool-call',
|
|
623
|
+
toolName: 'runShell',
|
|
624
|
+
input: JSON.stringify({ command: 'echo hello' }),
|
|
625
|
+
},
|
|
626
|
+
{ type: 'text', text: 'Command executed.' },
|
|
627
|
+
]),
|
|
628
|
+
tools: {
|
|
629
|
+
runShell: tool({
|
|
630
|
+
description: 'Run a shell command in the sandbox.',
|
|
631
|
+
inputSchema: z.object({ command: z.string() }),
|
|
632
|
+
execute: async ({ command }, { experimental_sandbox }) => {
|
|
633
|
+
if (experimental_sandbox == null) {
|
|
634
|
+
throw new Error('Sandbox is not available');
|
|
635
|
+
}
|
|
636
|
+
return experimental_sandbox.run({ command });
|
|
637
|
+
},
|
|
638
|
+
}),
|
|
639
|
+
},
|
|
640
|
+
instructions: 'You run shell commands in a sandbox.',
|
|
641
|
+
experimental_sandbox: constructorSandbox,
|
|
642
|
+
prepareStep: ({ stepNumber, experimental_sandbox }) => {
|
|
643
|
+
if (stepNumber === 0) {
|
|
644
|
+
firstPrepareStepSawConstructorSandbox =
|
|
645
|
+
experimental_sandbox === constructorSandbox;
|
|
646
|
+
return { experimental_sandbox: stepSandbox };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (experimental_sandbox === stepSandbox) {
|
|
650
|
+
prepareStepSawStepSandbox = true;
|
|
651
|
+
}
|
|
652
|
+
secondPrepareStepSawConstructorSandbox =
|
|
653
|
+
experimental_sandbox === constructorSandbox;
|
|
654
|
+
return {};
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const result = await agent.stream({
|
|
659
|
+
messages: [{ role: 'user', content: 'Run echo hello' }],
|
|
660
|
+
writable: getWritable(),
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
stepCount: result.steps.length,
|
|
665
|
+
lastStepText: result.steps[result.steps.length - 1]?.text,
|
|
666
|
+
toolResults: result.toolResults,
|
|
667
|
+
constructorSandboxRanCommand,
|
|
668
|
+
stepSandboxRanCommand,
|
|
669
|
+
firstPrepareStepSawConstructorSandbox,
|
|
670
|
+
secondPrepareStepSawConstructorSandbox,
|
|
671
|
+
prepareStepSawStepSandbox,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Experimental_SandboxSession as SandboxSession } from 'ai';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a minimal {@link SandboxSession} for tests. Only `run` returns a usable
|
|
5
|
+
* result by default; every other method throws so tests stay explicit about
|
|
6
|
+
* what they exercise. Pass `overrides` to customize individual methods.
|
|
7
|
+
*/
|
|
8
|
+
export function createTestSandbox(
|
|
9
|
+
overrides: Partial<SandboxSession> = {},
|
|
10
|
+
): SandboxSession {
|
|
11
|
+
const notImplemented = () => {
|
|
12
|
+
throw new Error('not implemented in test sandbox');
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
description: 'test sandbox',
|
|
16
|
+
readFile: notImplemented,
|
|
17
|
+
readBinaryFile: notImplemented,
|
|
18
|
+
readTextFile: notImplemented,
|
|
19
|
+
writeFile: notImplemented,
|
|
20
|
+
writeBinaryFile: notImplemented,
|
|
21
|
+
writeTextFile: notImplemented,
|
|
22
|
+
spawn: notImplemented,
|
|
23
|
+
run: async () => ({ exitCode: 0, stdout: '', stderr: '' }),
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
package/src/workflow-agent.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import {
|
|
9
9
|
getErrorMessage,
|
|
10
10
|
validateTypes,
|
|
11
|
+
withUserAgentSuffix,
|
|
11
12
|
type Context,
|
|
12
13
|
type HasRequiredKey,
|
|
13
14
|
type InferToolSetContext,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
type Prompt,
|
|
33
34
|
type TelemetryOptions as CoreTelemetryOptions,
|
|
34
35
|
type Instructions,
|
|
36
|
+
type Experimental_SandboxSession as SandboxSession,
|
|
35
37
|
} from 'ai';
|
|
36
38
|
import {
|
|
37
39
|
createRestrictedTelemetryDispatcher,
|
|
@@ -278,6 +280,11 @@ export interface PrepareStepInfo<
|
|
|
278
280
|
* `prepareStep` to update it for the current and subsequent steps.
|
|
279
281
|
*/
|
|
280
282
|
toolsContext: InferToolSetContext<TTools>;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The sandbox environment that the step is operating in.
|
|
286
|
+
*/
|
|
287
|
+
experimental_sandbox?: SandboxSession;
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
/**
|
|
@@ -326,6 +333,11 @@ export interface PrepareStepResult<
|
|
|
326
333
|
* Returning a value replaces the agent's tools context.
|
|
327
334
|
*/
|
|
328
335
|
toolsContext?: InferToolSetContext<TTools>;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Override the sandbox environment for this step.
|
|
339
|
+
*/
|
|
340
|
+
experimental_sandbox?: SandboxSession;
|
|
329
341
|
}
|
|
330
342
|
|
|
331
343
|
/**
|
|
@@ -494,6 +506,14 @@ export type WorkflowAgentOptions<
|
|
|
494
506
|
*/
|
|
495
507
|
experimental_download?: DownloadFunction;
|
|
496
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Default sandbox environment passed through to tool execution as
|
|
511
|
+
* `experimental_sandbox`.
|
|
512
|
+
*
|
|
513
|
+
* Per-stream `experimental_sandbox` values passed to `stream()` override this default.
|
|
514
|
+
*/
|
|
515
|
+
experimental_sandbox?: SandboxSession;
|
|
516
|
+
|
|
497
517
|
/**
|
|
498
518
|
* Default callback function called before each step in the agent loop.
|
|
499
519
|
* Use this to modify settings, manage context, or inject messages dynamically
|
|
@@ -933,6 +953,12 @@ export type WorkflowAgentStreamOptions<
|
|
|
933
953
|
*/
|
|
934
954
|
experimental_download?: DownloadFunction;
|
|
935
955
|
|
|
956
|
+
/**
|
|
957
|
+
* Sandbox environment passed through to tool execution as
|
|
958
|
+
* `experimental_sandbox`. Overrides the constructor-level value if provided.
|
|
959
|
+
*/
|
|
960
|
+
experimental_sandbox?: SandboxSession;
|
|
961
|
+
|
|
936
962
|
/**
|
|
937
963
|
* Callback function to be called after each step completes.
|
|
938
964
|
*/
|
|
@@ -1177,6 +1203,7 @@ export class WorkflowAgent<
|
|
|
1177
1203
|
private output?: OutputSpecification<any, any>;
|
|
1178
1204
|
private experimentalRepairToolCall?: ToolCallRepairFunction<TBaseTools>;
|
|
1179
1205
|
private experimentalDownload?: DownloadFunction;
|
|
1206
|
+
private experimentalSandbox?: SandboxSession;
|
|
1180
1207
|
private prepareStep?: PrepareStepCallback<TBaseTools, TRuntimeContext>;
|
|
1181
1208
|
private allowSystemInMessages: boolean;
|
|
1182
1209
|
private constructorOnStepEnd?: WorkflowAgentOnStepEndCallback<
|
|
@@ -1214,6 +1241,7 @@ export class WorkflowAgent<
|
|
|
1214
1241
|
this.output = options.output;
|
|
1215
1242
|
this.experimentalRepairToolCall = options.experimental_repairToolCall;
|
|
1216
1243
|
this.experimentalDownload = options.experimental_download;
|
|
1244
|
+
this.experimentalSandbox = options.experimental_sandbox;
|
|
1217
1245
|
this.prepareStep = options.prepareStep;
|
|
1218
1246
|
this.constructorOnStepEnd = options.onStepEnd ?? options.onStepFinish;
|
|
1219
1247
|
const { onFinish, onEnd = onFinish } = options;
|
|
@@ -1360,6 +1388,7 @@ export class WorkflowAgent<
|
|
|
1360
1388
|
: { messages: effectiveMessages! }),
|
|
1361
1389
|
} as Prompt);
|
|
1362
1390
|
const download = options.experimental_download ?? this.experimentalDownload;
|
|
1391
|
+
const sandbox = options.experimental_sandbox ?? this.experimentalSandbox;
|
|
1363
1392
|
|
|
1364
1393
|
// Process tool approval responses before starting the agent loop.
|
|
1365
1394
|
// This mirrors how stream-text.ts handles tool-approval-response parts:
|
|
@@ -1517,6 +1546,7 @@ export class WorkflowAgent<
|
|
|
1517
1546
|
toolCallId: approval.toolCallId,
|
|
1518
1547
|
messages: [],
|
|
1519
1548
|
context: resolvedContext,
|
|
1549
|
+
experimental_sandbox: sandbox,
|
|
1520
1550
|
});
|
|
1521
1551
|
const toolResult =
|
|
1522
1552
|
telemetryDispatcher.executeTool != null
|
|
@@ -1719,6 +1749,14 @@ export class WorkflowAgent<
|
|
|
1719
1749
|
}),
|
|
1720
1750
|
};
|
|
1721
1751
|
|
|
1752
|
+
// tag the outgoing request so usage can be attributed to WorkflowAgent.
|
|
1753
|
+
// chains with the `ai/<version>` and `ai-sdk/<provider>/<version>` suffixes
|
|
1754
|
+
// added downstream by the model run and the provider.
|
|
1755
|
+
mergedGenerationSettings.headers = withUserAgentSuffix(
|
|
1756
|
+
mergedGenerationSettings.headers ?? {},
|
|
1757
|
+
'ai-sdk-agent/workflow',
|
|
1758
|
+
);
|
|
1759
|
+
|
|
1722
1760
|
// Merge constructor + stream callbacks (constructor first, then stream)
|
|
1723
1761
|
const mergedOnStepEnd = mergeCallbacks(
|
|
1724
1762
|
this.constructorOnStepEnd as
|
|
@@ -1821,6 +1859,7 @@ export class WorkflowAgent<
|
|
|
1821
1859
|
messages: LanguageModelV4Prompt,
|
|
1822
1860
|
perToolContexts: Record<string, Context | undefined>,
|
|
1823
1861
|
currentStepNumber: number = 0,
|
|
1862
|
+
stepSandbox?: SandboxSession,
|
|
1824
1863
|
): Promise<WorkflowToolExecutionResult> => {
|
|
1825
1864
|
const toolCallEvent: ToolCall = {
|
|
1826
1865
|
type: 'tool-call',
|
|
@@ -1862,7 +1901,14 @@ export class WorkflowAgent<
|
|
|
1862
1901
|
let result: WorkflowToolExecutionResult;
|
|
1863
1902
|
try {
|
|
1864
1903
|
const execute = () =>
|
|
1865
|
-
executeTool(
|
|
1904
|
+
executeTool(
|
|
1905
|
+
toolCall,
|
|
1906
|
+
tools,
|
|
1907
|
+
messages,
|
|
1908
|
+
resolvedContext,
|
|
1909
|
+
download,
|
|
1910
|
+
stepSandbox,
|
|
1911
|
+
);
|
|
1866
1912
|
result =
|
|
1867
1913
|
telemetryDispatcher.executeTool != null
|
|
1868
1914
|
? await telemetryDispatcher.executeTool({
|
|
@@ -2034,6 +2080,7 @@ export class WorkflowAgent<
|
|
|
2034
2080
|
| ToolCallRepairFunction<ToolSet>
|
|
2035
2081
|
| undefined,
|
|
2036
2082
|
responseFormat: await (options.output ?? this.output)?.responseFormat,
|
|
2083
|
+
experimental_sandbox: sandbox,
|
|
2037
2084
|
});
|
|
2038
2085
|
|
|
2039
2086
|
// Track the final conversation messages from the iterator
|
|
@@ -2059,8 +2106,10 @@ export class WorkflowAgent<
|
|
|
2059
2106
|
step,
|
|
2060
2107
|
runtimeContext: yieldedRuntimeContext,
|
|
2061
2108
|
toolsContext: yieldedToolsContext,
|
|
2109
|
+
experimental_sandbox: stepSandbox,
|
|
2062
2110
|
providerExecutedToolResults,
|
|
2063
2111
|
} = result.value;
|
|
2112
|
+
const toolExecutionSandbox = stepSandbox ?? sandbox;
|
|
2064
2113
|
// Capture current step number before pushing (0-based)
|
|
2065
2114
|
const currentStepNumber = steps.length;
|
|
2066
2115
|
if (step) {
|
|
@@ -2141,6 +2190,7 @@ export class WorkflowAgent<
|
|
|
2141
2190
|
iterMessages,
|
|
2142
2191
|
toolsContext,
|
|
2143
2192
|
currentStepNumber,
|
|
2193
|
+
toolExecutionSandbox,
|
|
2144
2194
|
),
|
|
2145
2195
|
),
|
|
2146
2196
|
);
|
|
@@ -2280,6 +2330,7 @@ export class WorkflowAgent<
|
|
|
2280
2330
|
iterMessages,
|
|
2281
2331
|
toolsContext,
|
|
2282
2332
|
currentStepNumber,
|
|
2333
|
+
toolExecutionSandbox,
|
|
2283
2334
|
),
|
|
2284
2335
|
),
|
|
2285
2336
|
);
|
|
@@ -2757,6 +2808,7 @@ async function executeTool(
|
|
|
2757
2808
|
messages: LanguageModelV4Prompt,
|
|
2758
2809
|
context?: unknown,
|
|
2759
2810
|
download?: DownloadFunction,
|
|
2811
|
+
sandbox?: SandboxSession,
|
|
2760
2812
|
): Promise<WorkflowToolExecutionResult> {
|
|
2761
2813
|
const tool = tools[toolCall.toolName];
|
|
2762
2814
|
if (!tool) throw new Error(`Tool "${toolCall.toolName}" not found`);
|
|
@@ -2783,6 +2835,7 @@ async function executeTool(
|
|
|
2783
2835
|
messages,
|
|
2784
2836
|
// Pass per-tool context to the tool (resolved from `toolsContext`)
|
|
2785
2837
|
context,
|
|
2838
|
+
experimental_sandbox: sandbox,
|
|
2786
2839
|
});
|
|
2787
2840
|
} catch (error) {
|
|
2788
2841
|
// Convert tool errors to error-text results sent back to the model,
|