@ai-sdk/workflow 1.0.0-beta.102 → 1.0.0-beta.104
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 +15 -0
- package/dist/index.d.mts +22 -1
- package/dist/index.mjs +75 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- 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 +45 -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.104",
|
|
4
4
|
"description": "WorkflowAgent for building AI agents with AI SDK",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"ajv": "^8.20.0",
|
|
30
|
-
"@ai-sdk/provider": "4.0.0-beta.
|
|
31
|
-
"@ai-sdk/provider-utils": "5.0.0-beta.
|
|
32
|
-
"ai": "7.0.0-beta.
|
|
30
|
+
"@ai-sdk/provider": "4.0.0-beta.20",
|
|
31
|
+
"@ai-sdk/provider-utils": "5.0.0-beta.50",
|
|
32
|
+
"ai": "7.0.0-beta.184"
|
|
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
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type Prompt,
|
|
33
33
|
type TelemetryOptions as CoreTelemetryOptions,
|
|
34
34
|
type Instructions,
|
|
35
|
+
type Experimental_SandboxSession as SandboxSession,
|
|
35
36
|
} from 'ai';
|
|
36
37
|
import {
|
|
37
38
|
createRestrictedTelemetryDispatcher,
|
|
@@ -278,6 +279,11 @@ export interface PrepareStepInfo<
|
|
|
278
279
|
* `prepareStep` to update it for the current and subsequent steps.
|
|
279
280
|
*/
|
|
280
281
|
toolsContext: InferToolSetContext<TTools>;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The sandbox environment that the step is operating in.
|
|
285
|
+
*/
|
|
286
|
+
experimental_sandbox?: SandboxSession;
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
/**
|
|
@@ -326,6 +332,11 @@ export interface PrepareStepResult<
|
|
|
326
332
|
* Returning a value replaces the agent's tools context.
|
|
327
333
|
*/
|
|
328
334
|
toolsContext?: InferToolSetContext<TTools>;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Override the sandbox environment for this step.
|
|
338
|
+
*/
|
|
339
|
+
experimental_sandbox?: SandboxSession;
|
|
329
340
|
}
|
|
330
341
|
|
|
331
342
|
/**
|
|
@@ -494,6 +505,14 @@ export type WorkflowAgentOptions<
|
|
|
494
505
|
*/
|
|
495
506
|
experimental_download?: DownloadFunction;
|
|
496
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Default sandbox environment passed through to tool execution as
|
|
510
|
+
* `experimental_sandbox`.
|
|
511
|
+
*
|
|
512
|
+
* Per-stream `experimental_sandbox` values passed to `stream()` override this default.
|
|
513
|
+
*/
|
|
514
|
+
experimental_sandbox?: SandboxSession;
|
|
515
|
+
|
|
497
516
|
/**
|
|
498
517
|
* Default callback function called before each step in the agent loop.
|
|
499
518
|
* Use this to modify settings, manage context, or inject messages dynamically
|
|
@@ -933,6 +952,12 @@ export type WorkflowAgentStreamOptions<
|
|
|
933
952
|
*/
|
|
934
953
|
experimental_download?: DownloadFunction;
|
|
935
954
|
|
|
955
|
+
/**
|
|
956
|
+
* Sandbox environment passed through to tool execution as
|
|
957
|
+
* `experimental_sandbox`. Overrides the constructor-level value if provided.
|
|
958
|
+
*/
|
|
959
|
+
experimental_sandbox?: SandboxSession;
|
|
960
|
+
|
|
936
961
|
/**
|
|
937
962
|
* Callback function to be called after each step completes.
|
|
938
963
|
*/
|
|
@@ -1177,6 +1202,7 @@ export class WorkflowAgent<
|
|
|
1177
1202
|
private output?: OutputSpecification<any, any>;
|
|
1178
1203
|
private experimentalRepairToolCall?: ToolCallRepairFunction<TBaseTools>;
|
|
1179
1204
|
private experimentalDownload?: DownloadFunction;
|
|
1205
|
+
private experimentalSandbox?: SandboxSession;
|
|
1180
1206
|
private prepareStep?: PrepareStepCallback<TBaseTools, TRuntimeContext>;
|
|
1181
1207
|
private allowSystemInMessages: boolean;
|
|
1182
1208
|
private constructorOnStepEnd?: WorkflowAgentOnStepEndCallback<
|
|
@@ -1214,6 +1240,7 @@ export class WorkflowAgent<
|
|
|
1214
1240
|
this.output = options.output;
|
|
1215
1241
|
this.experimentalRepairToolCall = options.experimental_repairToolCall;
|
|
1216
1242
|
this.experimentalDownload = options.experimental_download;
|
|
1243
|
+
this.experimentalSandbox = options.experimental_sandbox;
|
|
1217
1244
|
this.prepareStep = options.prepareStep;
|
|
1218
1245
|
this.constructorOnStepEnd = options.onStepEnd ?? options.onStepFinish;
|
|
1219
1246
|
const { onFinish, onEnd = onFinish } = options;
|
|
@@ -1360,6 +1387,7 @@ export class WorkflowAgent<
|
|
|
1360
1387
|
: { messages: effectiveMessages! }),
|
|
1361
1388
|
} as Prompt);
|
|
1362
1389
|
const download = options.experimental_download ?? this.experimentalDownload;
|
|
1390
|
+
const sandbox = options.experimental_sandbox ?? this.experimentalSandbox;
|
|
1363
1391
|
|
|
1364
1392
|
// Process tool approval responses before starting the agent loop.
|
|
1365
1393
|
// This mirrors how stream-text.ts handles tool-approval-response parts:
|
|
@@ -1517,6 +1545,7 @@ export class WorkflowAgent<
|
|
|
1517
1545
|
toolCallId: approval.toolCallId,
|
|
1518
1546
|
messages: [],
|
|
1519
1547
|
context: resolvedContext,
|
|
1548
|
+
experimental_sandbox: sandbox,
|
|
1520
1549
|
});
|
|
1521
1550
|
const toolResult =
|
|
1522
1551
|
telemetryDispatcher.executeTool != null
|
|
@@ -1821,6 +1850,7 @@ export class WorkflowAgent<
|
|
|
1821
1850
|
messages: LanguageModelV4Prompt,
|
|
1822
1851
|
perToolContexts: Record<string, Context | undefined>,
|
|
1823
1852
|
currentStepNumber: number = 0,
|
|
1853
|
+
stepSandbox?: SandboxSession,
|
|
1824
1854
|
): Promise<WorkflowToolExecutionResult> => {
|
|
1825
1855
|
const toolCallEvent: ToolCall = {
|
|
1826
1856
|
type: 'tool-call',
|
|
@@ -1862,7 +1892,14 @@ export class WorkflowAgent<
|
|
|
1862
1892
|
let result: WorkflowToolExecutionResult;
|
|
1863
1893
|
try {
|
|
1864
1894
|
const execute = () =>
|
|
1865
|
-
executeTool(
|
|
1895
|
+
executeTool(
|
|
1896
|
+
toolCall,
|
|
1897
|
+
tools,
|
|
1898
|
+
messages,
|
|
1899
|
+
resolvedContext,
|
|
1900
|
+
download,
|
|
1901
|
+
stepSandbox,
|
|
1902
|
+
);
|
|
1866
1903
|
result =
|
|
1867
1904
|
telemetryDispatcher.executeTool != null
|
|
1868
1905
|
? await telemetryDispatcher.executeTool({
|
|
@@ -2034,6 +2071,7 @@ export class WorkflowAgent<
|
|
|
2034
2071
|
| ToolCallRepairFunction<ToolSet>
|
|
2035
2072
|
| undefined,
|
|
2036
2073
|
responseFormat: await (options.output ?? this.output)?.responseFormat,
|
|
2074
|
+
experimental_sandbox: sandbox,
|
|
2037
2075
|
});
|
|
2038
2076
|
|
|
2039
2077
|
// Track the final conversation messages from the iterator
|
|
@@ -2059,8 +2097,10 @@ export class WorkflowAgent<
|
|
|
2059
2097
|
step,
|
|
2060
2098
|
runtimeContext: yieldedRuntimeContext,
|
|
2061
2099
|
toolsContext: yieldedToolsContext,
|
|
2100
|
+
experimental_sandbox: stepSandbox,
|
|
2062
2101
|
providerExecutedToolResults,
|
|
2063
2102
|
} = result.value;
|
|
2103
|
+
const toolExecutionSandbox = stepSandbox ?? sandbox;
|
|
2064
2104
|
// Capture current step number before pushing (0-based)
|
|
2065
2105
|
const currentStepNumber = steps.length;
|
|
2066
2106
|
if (step) {
|
|
@@ -2141,6 +2181,7 @@ export class WorkflowAgent<
|
|
|
2141
2181
|
iterMessages,
|
|
2142
2182
|
toolsContext,
|
|
2143
2183
|
currentStepNumber,
|
|
2184
|
+
toolExecutionSandbox,
|
|
2144
2185
|
),
|
|
2145
2186
|
),
|
|
2146
2187
|
);
|
|
@@ -2280,6 +2321,7 @@ export class WorkflowAgent<
|
|
|
2280
2321
|
iterMessages,
|
|
2281
2322
|
toolsContext,
|
|
2282
2323
|
currentStepNumber,
|
|
2324
|
+
toolExecutionSandbox,
|
|
2283
2325
|
),
|
|
2284
2326
|
),
|
|
2285
2327
|
);
|
|
@@ -2757,6 +2799,7 @@ async function executeTool(
|
|
|
2757
2799
|
messages: LanguageModelV4Prompt,
|
|
2758
2800
|
context?: unknown,
|
|
2759
2801
|
download?: DownloadFunction,
|
|
2802
|
+
sandbox?: SandboxSession,
|
|
2760
2803
|
): Promise<WorkflowToolExecutionResult> {
|
|
2761
2804
|
const tool = tools[toolCall.toolName];
|
|
2762
2805
|
if (!tool) throw new Error(`Tool "${toolCall.toolName}" not found`);
|
|
@@ -2783,6 +2826,7 @@ async function executeTool(
|
|
|
2783
2826
|
messages,
|
|
2784
2827
|
// Pass per-tool context to the tool (resolved from `toolsContext`)
|
|
2785
2828
|
context,
|
|
2829
|
+
experimental_sandbox: sandbox,
|
|
2786
2830
|
});
|
|
2787
2831
|
} catch (error) {
|
|
2788
2832
|
// Convert tool errors to error-text results sent back to the model,
|