@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/workflow",
3
- "version": "1.0.0-beta.103",
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.184"
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
+ }
@@ -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(toolCall, tools, messages, resolvedContext, download);
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,