@ai-sdk/workflow 1.0.0-canary.93 → 1.0.0
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 +168 -0
- package/dist/index.d.mts +32 -1
- package/dist/index.mjs +109 -62
- 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 +113 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/workflow",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
|
31
|
-
"@ai-sdk/provider-utils": "5.0.0
|
|
32
|
-
"ai": "7.0.0
|
|
30
|
+
"@ai-sdk/provider": "4.0.0",
|
|
31
|
+
"@ai-sdk/provider-utils": "5.0.0",
|
|
32
|
+
"ai": "7.0.0"
|
|
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
|
|
@@ -559,6 +579,16 @@ export type WorkflowAgentOptions<
|
|
|
559
579
|
* model, tools, instructions, or other settings based on runtime context.
|
|
560
580
|
*/
|
|
561
581
|
prepareCall?: PrepareCallCallback<TTools, TRuntimeContext>;
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Whether to allow system messages inside the `prompt` or `messages` fields.
|
|
585
|
+
* When `false` (the default), system messages in `prompt` or `messages` are
|
|
586
|
+
* rejected to prevent prompt-injection attacks. Set to `true` only when you
|
|
587
|
+
* intentionally interleave system messages with user messages.
|
|
588
|
+
*
|
|
589
|
+
* @default false
|
|
590
|
+
*/
|
|
591
|
+
allowSystemInMessages?: boolean;
|
|
562
592
|
};
|
|
563
593
|
|
|
564
594
|
/**
|
|
@@ -923,6 +953,12 @@ export type WorkflowAgentStreamOptions<
|
|
|
923
953
|
*/
|
|
924
954
|
experimental_download?: DownloadFunction;
|
|
925
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
|
+
|
|
926
962
|
/**
|
|
927
963
|
* Callback function to be called after each step completes.
|
|
928
964
|
*/
|
|
@@ -1167,7 +1203,9 @@ export class WorkflowAgent<
|
|
|
1167
1203
|
private output?: OutputSpecification<any, any>;
|
|
1168
1204
|
private experimentalRepairToolCall?: ToolCallRepairFunction<TBaseTools>;
|
|
1169
1205
|
private experimentalDownload?: DownloadFunction;
|
|
1206
|
+
private experimentalSandbox?: SandboxSession;
|
|
1170
1207
|
private prepareStep?: PrepareStepCallback<TBaseTools, TRuntimeContext>;
|
|
1208
|
+
private allowSystemInMessages: boolean;
|
|
1171
1209
|
private constructorOnStepEnd?: WorkflowAgentOnStepEndCallback<
|
|
1172
1210
|
TBaseTools,
|
|
1173
1211
|
TRuntimeContext
|
|
@@ -1203,6 +1241,7 @@ export class WorkflowAgent<
|
|
|
1203
1241
|
this.output = options.output;
|
|
1204
1242
|
this.experimentalRepairToolCall = options.experimental_repairToolCall;
|
|
1205
1243
|
this.experimentalDownload = options.experimental_download;
|
|
1244
|
+
this.experimentalSandbox = options.experimental_sandbox;
|
|
1206
1245
|
this.prepareStep = options.prepareStep;
|
|
1207
1246
|
this.constructorOnStepEnd = options.onStepEnd ?? options.onStepFinish;
|
|
1208
1247
|
const { onFinish, onEnd = onFinish } = options;
|
|
@@ -1212,6 +1251,7 @@ export class WorkflowAgent<
|
|
|
1212
1251
|
this.constructorOnToolExecutionStart = options.onToolExecutionStart;
|
|
1213
1252
|
this.constructorOnToolExecutionEnd = options.onToolExecutionEnd;
|
|
1214
1253
|
this.prepareCall = options.prepareCall;
|
|
1254
|
+
this.allowSystemInMessages = options.allowSystemInMessages ?? false;
|
|
1215
1255
|
|
|
1216
1256
|
// Extract generation settings
|
|
1217
1257
|
this.generationSettings = {
|
|
@@ -1342,12 +1382,13 @@ export class WorkflowAgent<
|
|
|
1342
1382
|
|
|
1343
1383
|
const prompt = await standardizePrompt({
|
|
1344
1384
|
system: effectiveInstructions,
|
|
1345
|
-
allowSystemInMessages:
|
|
1385
|
+
allowSystemInMessages: this.allowSystemInMessages,
|
|
1346
1386
|
...(effectivePrompt != null
|
|
1347
1387
|
? { prompt: effectivePrompt }
|
|
1348
1388
|
: { messages: effectiveMessages! }),
|
|
1349
1389
|
} as Prompt);
|
|
1350
1390
|
const download = options.experimental_download ?? this.experimentalDownload;
|
|
1391
|
+
const sandbox = options.experimental_sandbox ?? this.experimentalSandbox;
|
|
1351
1392
|
|
|
1352
1393
|
// Process tool approval responses before starting the agent loop.
|
|
1353
1394
|
// This mirrors how stream-text.ts handles tool-approval-response parts:
|
|
@@ -1366,6 +1407,7 @@ export class WorkflowAgent<
|
|
|
1366
1407
|
toolName: collected.toolCall.toolName,
|
|
1367
1408
|
input: collected.toolCall.input,
|
|
1368
1409
|
reason: collected.approvalResponse.reason,
|
|
1410
|
+
providerExecuted: collected.toolCall.providerExecuted === true,
|
|
1369
1411
|
collected,
|
|
1370
1412
|
}),
|
|
1371
1413
|
);
|
|
@@ -1375,9 +1417,26 @@ export class WorkflowAgent<
|
|
|
1375
1417
|
toolName: collected.toolCall.toolName,
|
|
1376
1418
|
input: collected.toolCall.input,
|
|
1377
1419
|
reason: collected.approvalResponse.reason,
|
|
1420
|
+
providerExecuted: collected.toolCall.providerExecuted === true,
|
|
1378
1421
|
}),
|
|
1379
1422
|
);
|
|
1380
1423
|
|
|
1424
|
+
// Approval ids of provider-executed tool calls. Provider-executed tools
|
|
1425
|
+
// (e.g. MCP via the Responses API) cannot be resolved locally — the
|
|
1426
|
+
// provider owns execution. We therefore skip them from local execution
|
|
1427
|
+
// and preserve their approval responses in the messages so the provider
|
|
1428
|
+
// receives the approval on the next call. The discriminator is sourced
|
|
1429
|
+
// from the original `tool-call` part (matching how core's stream-text.ts
|
|
1430
|
+
// decides), not from the response part which may be missing the flag.
|
|
1431
|
+
const providerExecutedApprovalIds = new Set<string>(
|
|
1432
|
+
[
|
|
1433
|
+
...collectedApprovals.approvedToolApprovals,
|
|
1434
|
+
...collectedApprovals.deniedToolApprovals,
|
|
1435
|
+
]
|
|
1436
|
+
.filter(collected => collected.toolCall.providerExecuted === true)
|
|
1437
|
+
.map(collected => collected.approvalResponse.approvalId),
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1381
1440
|
if (approvedToolApprovals.length > 0 || deniedToolApprovals.length > 0) {
|
|
1382
1441
|
const _toolResultMessages: ModelMessage[] = [];
|
|
1383
1442
|
const toolResultContent: LanguageModelV4ToolResultPart[] = [];
|
|
@@ -1390,6 +1449,11 @@ export class WorkflowAgent<
|
|
|
1390
1449
|
|
|
1391
1450
|
// Execute approved tools
|
|
1392
1451
|
for (const approval of approvedToolApprovals) {
|
|
1452
|
+
// Provider-executed approvals are forwarded to the provider via the
|
|
1453
|
+
// preserved approval response below, not executed locally.
|
|
1454
|
+
if (approval.providerExecuted) {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1393
1457
|
const tool = (this.tools as ToolSet)[approval.toolName];
|
|
1394
1458
|
if (tool && typeof tool.execute === 'function') {
|
|
1395
1459
|
if (!tool.needsApproval) {
|
|
@@ -1482,6 +1546,7 @@ export class WorkflowAgent<
|
|
|
1482
1546
|
toolCallId: approval.toolCallId,
|
|
1483
1547
|
messages: [],
|
|
1484
1548
|
context: resolvedContext,
|
|
1549
|
+
experimental_sandbox: sandbox,
|
|
1485
1550
|
});
|
|
1486
1551
|
const toolResult =
|
|
1487
1552
|
telemetryDispatcher.executeTool != null
|
|
@@ -1564,6 +1629,11 @@ export class WorkflowAgent<
|
|
|
1564
1629
|
|
|
1565
1630
|
// Create denial results for denied tools
|
|
1566
1631
|
for (const denial of deniedToolApprovals) {
|
|
1632
|
+
// Provider-executed denials are forwarded to the provider via the
|
|
1633
|
+
// preserved approval response below, not turned into a local result.
|
|
1634
|
+
if (denial.providerExecuted) {
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1567
1637
|
toolResultContent.push({
|
|
1568
1638
|
type: 'tool-result' as const,
|
|
1569
1639
|
toolCallId: denial.toolCallId,
|
|
@@ -1575,20 +1645,33 @@ export class WorkflowAgent<
|
|
|
1575
1645
|
});
|
|
1576
1646
|
}
|
|
1577
1647
|
|
|
1578
|
-
// Strip approval parts
|
|
1648
|
+
// Strip approval parts that we resolved locally and inject tool results.
|
|
1649
|
+
// Provider-executed approval parts are preserved so the next call to
|
|
1650
|
+
// `convertToLanguageModelPrompt` forwards the approval response to the
|
|
1651
|
+
// provider (it only forwards responses flagged `providerExecuted`).
|
|
1579
1652
|
const cleanedMessages: ModelMessage[] = [];
|
|
1580
1653
|
for (const msg of prompt.messages) {
|
|
1581
1654
|
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
1582
1655
|
const filtered = (msg.content as any[]).filter(
|
|
1583
|
-
(p: any) =>
|
|
1656
|
+
(p: any) =>
|
|
1657
|
+
p.type !== 'tool-approval-request' ||
|
|
1658
|
+
providerExecutedApprovalIds.has(p.approvalId),
|
|
1584
1659
|
);
|
|
1585
1660
|
if (filtered.length > 0) {
|
|
1586
1661
|
cleanedMessages.push({ ...msg, content: filtered });
|
|
1587
1662
|
}
|
|
1588
1663
|
} else if (msg.role === 'tool') {
|
|
1589
|
-
const filtered = (msg.content as any[]).
|
|
1590
|
-
(p
|
|
1591
|
-
|
|
1664
|
+
const filtered = (msg.content as any[]).flatMap((p: any) => {
|
|
1665
|
+
if (p.type !== 'tool-approval-response') {
|
|
1666
|
+
return [p];
|
|
1667
|
+
}
|
|
1668
|
+
if (!providerExecutedApprovalIds.has(p.approvalId)) {
|
|
1669
|
+
return [];
|
|
1670
|
+
}
|
|
1671
|
+
// Re-stamp `providerExecuted` so the conversion layer forwards the
|
|
1672
|
+
// response even if the client omitted the flag on the response part.
|
|
1673
|
+
return [{ ...p, providerExecuted: true }];
|
|
1674
|
+
});
|
|
1592
1675
|
if (filtered.length > 0) {
|
|
1593
1676
|
cleanedMessages.push({ ...msg, content: filtered });
|
|
1594
1677
|
}
|
|
@@ -1666,6 +1749,14 @@ export class WorkflowAgent<
|
|
|
1666
1749
|
}),
|
|
1667
1750
|
};
|
|
1668
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
|
+
|
|
1669
1760
|
// Merge constructor + stream callbacks (constructor first, then stream)
|
|
1670
1761
|
const mergedOnStepEnd = mergeCallbacks(
|
|
1671
1762
|
this.constructorOnStepEnd as
|
|
@@ -1768,6 +1859,7 @@ export class WorkflowAgent<
|
|
|
1768
1859
|
messages: LanguageModelV4Prompt,
|
|
1769
1860
|
perToolContexts: Record<string, Context | undefined>,
|
|
1770
1861
|
currentStepNumber: number = 0,
|
|
1862
|
+
stepSandbox?: SandboxSession,
|
|
1771
1863
|
): Promise<WorkflowToolExecutionResult> => {
|
|
1772
1864
|
const toolCallEvent: ToolCall = {
|
|
1773
1865
|
type: 'tool-call',
|
|
@@ -1809,7 +1901,14 @@ export class WorkflowAgent<
|
|
|
1809
1901
|
let result: WorkflowToolExecutionResult;
|
|
1810
1902
|
try {
|
|
1811
1903
|
const execute = () =>
|
|
1812
|
-
executeTool(
|
|
1904
|
+
executeTool(
|
|
1905
|
+
toolCall,
|
|
1906
|
+
tools,
|
|
1907
|
+
messages,
|
|
1908
|
+
resolvedContext,
|
|
1909
|
+
download,
|
|
1910
|
+
stepSandbox,
|
|
1911
|
+
);
|
|
1813
1912
|
result =
|
|
1814
1913
|
telemetryDispatcher.executeTool != null
|
|
1815
1914
|
? await telemetryDispatcher.executeTool({
|
|
@@ -1981,6 +2080,7 @@ export class WorkflowAgent<
|
|
|
1981
2080
|
| ToolCallRepairFunction<ToolSet>
|
|
1982
2081
|
| undefined,
|
|
1983
2082
|
responseFormat: await (options.output ?? this.output)?.responseFormat,
|
|
2083
|
+
experimental_sandbox: sandbox,
|
|
1984
2084
|
});
|
|
1985
2085
|
|
|
1986
2086
|
// Track the final conversation messages from the iterator
|
|
@@ -2006,8 +2106,10 @@ export class WorkflowAgent<
|
|
|
2006
2106
|
step,
|
|
2007
2107
|
runtimeContext: yieldedRuntimeContext,
|
|
2008
2108
|
toolsContext: yieldedToolsContext,
|
|
2109
|
+
experimental_sandbox: stepSandbox,
|
|
2009
2110
|
providerExecutedToolResults,
|
|
2010
2111
|
} = result.value;
|
|
2112
|
+
const toolExecutionSandbox = stepSandbox ?? sandbox;
|
|
2011
2113
|
// Capture current step number before pushing (0-based)
|
|
2012
2114
|
const currentStepNumber = steps.length;
|
|
2013
2115
|
if (step) {
|
|
@@ -2088,6 +2190,7 @@ export class WorkflowAgent<
|
|
|
2088
2190
|
iterMessages,
|
|
2089
2191
|
toolsContext,
|
|
2090
2192
|
currentStepNumber,
|
|
2193
|
+
toolExecutionSandbox,
|
|
2091
2194
|
),
|
|
2092
2195
|
),
|
|
2093
2196
|
);
|
|
@@ -2227,6 +2330,7 @@ export class WorkflowAgent<
|
|
|
2227
2330
|
iterMessages,
|
|
2228
2331
|
toolsContext,
|
|
2229
2332
|
currentStepNumber,
|
|
2333
|
+
toolExecutionSandbox,
|
|
2230
2334
|
),
|
|
2231
2335
|
),
|
|
2232
2336
|
);
|
|
@@ -2704,6 +2808,7 @@ async function executeTool(
|
|
|
2704
2808
|
messages: LanguageModelV4Prompt,
|
|
2705
2809
|
context?: unknown,
|
|
2706
2810
|
download?: DownloadFunction,
|
|
2811
|
+
sandbox?: SandboxSession,
|
|
2707
2812
|
): Promise<WorkflowToolExecutionResult> {
|
|
2708
2813
|
const tool = tools[toolCall.toolName];
|
|
2709
2814
|
if (!tool) throw new Error(`Tool "${toolCall.toolName}" not found`);
|
|
@@ -2730,6 +2835,7 @@ async function executeTool(
|
|
|
2730
2835
|
messages,
|
|
2731
2836
|
// Pass per-tool context to the tool (resolved from `toolsContext`)
|
|
2732
2837
|
context,
|
|
2838
|
+
experimental_sandbox: sandbox,
|
|
2733
2839
|
});
|
|
2734
2840
|
} catch (error) {
|
|
2735
2841
|
// Convert tool errors to error-text results sent back to the model,
|