@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/workflow",
3
- "version": "1.0.0-beta.102",
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.19",
31
- "@ai-sdk/provider-utils": "5.0.0-beta.49",
32
- "ai": "7.0.0-beta.183"
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
+ }
@@ -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(toolCall, tools, messages, resolvedContext, download);
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,