@ai-sdk/harness 1.0.122 → 1.0.124
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 +27 -0
- package/dist/agent/index.d.ts +5 -5
- package/dist/agent/index.js +273 -64
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.js +0 -1
- package/dist/bridge/index.js.map +1 -1
- package/dist/index.d.ts +28 -6
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +30 -5
- package/dist/utils/index.js +130 -16
- package/dist/utils/index.js.map +1 -1
- package/package.json +4 -4
- package/src/agent/harness-agent.ts +32 -12
- package/src/agent/internal/bootstrap-recipe.ts +16 -16
- package/src/agent/internal/harness-stream-text-result.ts +1 -3
- package/src/agent/internal/run-prompt.ts +134 -19
- package/src/agent/internal/sandbox-bootstrap.ts +24 -10
- package/src/agent/internal/strip-work-dir.ts +57 -0
- package/src/agent/prepare-harness-sandbox-template.ts +16 -3
- package/src/agent/prepare-sandbox-for-harness.ts +14 -5
- package/src/bridge/index.ts +0 -1
- package/src/utils/sandbox-channel.ts +172 -19
- package/src/utils/write-instructions.ts +1 -1
- package/src/utils/write-skills.ts +1 -1
- package/src/v1/harness-session-data-directory-path.ts +28 -0
- package/src/v1/harness-v1-bootstrap.ts +5 -5
- package/src/v1/harness-v1-network-sandbox-session.ts +24 -0
- package/src/v1/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.124",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@ai-sdk/provider": "4.0.18",
|
|
48
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
49
|
-
"ai": "7.0.
|
|
48
|
+
"@ai-sdk/provider-utils": "5.0.47",
|
|
49
|
+
"ai": "7.0.114"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"ws": "^8.21.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@ai-sdk/otel": "1.0.
|
|
61
|
+
"@ai-sdk/otel": "1.0.114",
|
|
62
62
|
"@opentelemetry/sdk-trace-base": "2.7.1",
|
|
63
63
|
"@types/node": "22.19.19",
|
|
64
64
|
"@types/ws": "^8.5.13",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { HarnessCapabilityUnsupportedError } from '../errors/harness-capability-unsupported-error';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import {
|
|
3
|
+
harnessStateDirectoryPath,
|
|
4
|
+
type HarnessV1,
|
|
5
|
+
type HarnessV1BuiltinToolFiltering,
|
|
6
|
+
type HarnessV1JSONSchema,
|
|
7
|
+
type HarnessV1NetworkSandboxSession,
|
|
8
|
+
type HarnessV1ResponseFormat,
|
|
8
9
|
} from '../v1';
|
|
9
10
|
import {
|
|
10
11
|
asArray,
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
} from './internal/permission-mode';
|
|
69
70
|
import { resolveHarnessAgentToolFiltering } from './internal/tool-filtering';
|
|
70
71
|
import { resolveSandboxDefaultWorkingDirectory } from '../utils/resolve-sandbox-default-working-directory';
|
|
72
|
+
import { resolveSandboxHomeDir } from '../utils/sandbox-home-dir';
|
|
71
73
|
import { getRestrictedSandboxSession } from '../utils/get-restricted-sandbox-session';
|
|
72
74
|
import type { HarnessAgentLifecycleCallbacks } from './internal/turn-telemetry';
|
|
73
75
|
|
|
@@ -402,7 +404,14 @@ export class HarnessAgent<
|
|
|
402
404
|
session: toolSafeSandboxSession,
|
|
403
405
|
recipe,
|
|
404
406
|
identity: recipeIdentity,
|
|
405
|
-
|
|
407
|
+
// Harness infrastructure always lives under the sandbox's own
|
|
408
|
+
// HOME, never the session's working directory.
|
|
409
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
410
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
411
|
+
sandbox: toolSafeSandboxSession,
|
|
412
|
+
abortSignal,
|
|
413
|
+
}),
|
|
414
|
+
}),
|
|
406
415
|
abortSignal,
|
|
407
416
|
});
|
|
408
417
|
} catch (err) {
|
|
@@ -455,8 +464,14 @@ export class HarnessAgent<
|
|
|
455
464
|
session: resumedSandboxSession.restricted(),
|
|
456
465
|
recipe,
|
|
457
466
|
identity: recipeIdentity,
|
|
458
|
-
|
|
459
|
-
|
|
467
|
+
// Harness infrastructure always lives under the sandbox's own
|
|
468
|
+
// HOME, never the working directory.
|
|
469
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
470
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
471
|
+
sandbox: resumedSandboxSession,
|
|
472
|
+
abortSignal,
|
|
473
|
+
}),
|
|
474
|
+
}),
|
|
460
475
|
abortSignal,
|
|
461
476
|
});
|
|
462
477
|
} catch (err) {
|
|
@@ -503,8 +518,14 @@ export class HarnessAgent<
|
|
|
503
518
|
session: createdSandboxSession.restricted(),
|
|
504
519
|
recipe: sandboxBootstrapPlan.recipe,
|
|
505
520
|
identity: sandboxBootstrapPlan.recipeIdentity,
|
|
506
|
-
|
|
507
|
-
|
|
521
|
+
// Harness infrastructure always lives under the sandbox's own
|
|
522
|
+
// HOME, never the working directory.
|
|
523
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
524
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
525
|
+
sandbox: createdSandboxSession,
|
|
526
|
+
abortSignal,
|
|
527
|
+
}),
|
|
528
|
+
}),
|
|
508
529
|
abortSignal,
|
|
509
530
|
});
|
|
510
531
|
} catch (err) {
|
|
@@ -877,7 +898,6 @@ export class HarnessAgent<
|
|
|
877
898
|
};
|
|
878
899
|
}
|
|
879
900
|
}
|
|
880
|
-
return undefined;
|
|
881
901
|
}
|
|
882
902
|
|
|
883
903
|
/*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
3
|
-
import type
|
|
3
|
+
import { harnessStateDirectoryPath, type HarnessV1Bootstrap } from '../../v1';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Version of the bootstrap recipe shape itself. Bump to force every existing
|
|
@@ -10,7 +10,9 @@ export const BOOTSTRAP_SCHEMA_VERSION = 1;
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Deterministic 16-char hex identity derived from the recipe's content
|
|
13
|
-
* (harnessId, bootstrapDir, file paths + contents,
|
|
13
|
+
* (harness state directory, harnessId, bootstrapDir, file paths + contents,
|
|
14
|
+
* commands, schema version). The sandbox's concrete HOME is unavailable until
|
|
15
|
+
* creation, so the directory is hashed with a symbolic HOME prefix.
|
|
14
16
|
* Two adapters with equivalent recipes produce the same identity; any
|
|
15
17
|
* content change produces a different identity.
|
|
16
18
|
*
|
|
@@ -27,6 +29,7 @@ export async function hashHarnessBootstrap(
|
|
|
27
29
|
chunks.push(encoder.encode('\0'));
|
|
28
30
|
};
|
|
29
31
|
|
|
32
|
+
pushString(harnessStateDirectoryPath({ sandboxHomeDir: '$HOME' }));
|
|
30
33
|
pushString(recipe.harnessId);
|
|
31
34
|
pushString(recipe.bootstrapDir);
|
|
32
35
|
|
|
@@ -66,16 +69,16 @@ export async function hashHarnessBootstrap(
|
|
|
66
69
|
export function bootstrapMarkerPath({
|
|
67
70
|
recipe,
|
|
68
71
|
identity,
|
|
69
|
-
|
|
72
|
+
stateDirectory,
|
|
70
73
|
}: {
|
|
71
74
|
recipe: HarnessV1Bootstrap;
|
|
72
75
|
identity: string;
|
|
73
|
-
|
|
76
|
+
stateDirectory: string;
|
|
74
77
|
}): string {
|
|
75
78
|
return posix.join(
|
|
76
79
|
resolveBootstrapPath({
|
|
77
80
|
path: recipe.bootstrapDir,
|
|
78
|
-
|
|
81
|
+
stateDirectory,
|
|
79
82
|
}),
|
|
80
83
|
`.bootstrap-${identity}.ok`,
|
|
81
84
|
);
|
|
@@ -95,19 +98,19 @@ export async function applyBootstrapRecipe({
|
|
|
95
98
|
session,
|
|
96
99
|
recipe,
|
|
97
100
|
identity,
|
|
98
|
-
|
|
101
|
+
stateDirectory,
|
|
99
102
|
abortSignal,
|
|
100
103
|
}: {
|
|
101
104
|
session: SandboxSession;
|
|
102
105
|
recipe: HarnessV1Bootstrap;
|
|
103
106
|
identity: string;
|
|
104
|
-
|
|
107
|
+
stateDirectory: string;
|
|
105
108
|
abortSignal?: AbortSignal;
|
|
106
109
|
}): Promise<void> {
|
|
107
110
|
const markerPath = bootstrapMarkerPath({
|
|
108
111
|
recipe,
|
|
109
112
|
identity,
|
|
110
|
-
|
|
113
|
+
stateDirectory,
|
|
111
114
|
});
|
|
112
115
|
|
|
113
116
|
const existingMarker = await session.readTextFile({
|
|
@@ -120,11 +123,10 @@ export async function applyBootstrapRecipe({
|
|
|
120
123
|
|
|
121
124
|
const bootstrapDir = resolveBootstrapPath({
|
|
122
125
|
path: recipe.bootstrapDir,
|
|
123
|
-
|
|
126
|
+
stateDirectory,
|
|
124
127
|
});
|
|
125
128
|
const mkdirResult = await session.run({
|
|
126
129
|
command: 'mkdir -p "$BOOTSTRAP_DIR"',
|
|
127
|
-
workingDirectory: defaultWorkingDirectory,
|
|
128
130
|
env: { BOOTSTRAP_DIR: bootstrapDir },
|
|
129
131
|
abortSignal,
|
|
130
132
|
});
|
|
@@ -138,7 +140,7 @@ export async function applyBootstrapRecipe({
|
|
|
138
140
|
await session.writeTextFile({
|
|
139
141
|
path: resolveBootstrapPath({
|
|
140
142
|
path: file.path,
|
|
141
|
-
|
|
143
|
+
stateDirectory,
|
|
142
144
|
}),
|
|
143
145
|
content: file.content,
|
|
144
146
|
abortSignal,
|
|
@@ -167,12 +169,10 @@ export async function applyBootstrapRecipe({
|
|
|
167
169
|
|
|
168
170
|
function resolveBootstrapPath({
|
|
169
171
|
path,
|
|
170
|
-
|
|
172
|
+
stateDirectory,
|
|
171
173
|
}: {
|
|
172
174
|
path: string;
|
|
173
|
-
|
|
175
|
+
stateDirectory: string;
|
|
174
176
|
}): string {
|
|
175
|
-
return posix.isAbsolute(path)
|
|
176
|
-
? path
|
|
177
|
-
: posix.resolve(defaultWorkingDirectory, path);
|
|
177
|
+
return posix.isAbsolute(path) ? path : posix.resolve(stateDirectory, path);
|
|
178
178
|
}
|
|
@@ -853,10 +853,8 @@ export class HarnessStreamTextResult<
|
|
|
853
853
|
this.currentStepContent.push({
|
|
854
854
|
...(part as object),
|
|
855
855
|
} as ContentPart<TOOLS>);
|
|
856
|
-
return;
|
|
857
856
|
default:
|
|
858
|
-
|
|
859
|
-
return;
|
|
857
|
+
// Boundary, raw, error, and finish parts are not ContentParts.
|
|
860
858
|
}
|
|
861
859
|
}
|
|
862
860
|
}
|
|
@@ -53,7 +53,11 @@ import type { HarnessAgentToolApprovalConfiguration } from '../harness-agent-set
|
|
|
53
53
|
import { HarnessStreamTextResult } from './harness-stream-text-result';
|
|
54
54
|
import { getOwn } from './get-own';
|
|
55
55
|
import { translateStreamPart } from './translate-stream-part';
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
createToolInputWorkDirStripper,
|
|
58
|
+
stripParsedToolInputWorkDir,
|
|
59
|
+
stripWorkDir,
|
|
60
|
+
} from './strip-work-dir';
|
|
57
61
|
import {
|
|
58
62
|
createTurnLifecycle,
|
|
59
63
|
type HarnessAgentLifecycleCallbacks,
|
|
@@ -63,6 +67,8 @@ import { resolveCustomToolApproval } from './permission-mode';
|
|
|
63
67
|
import { logBridgeError } from '../../utils/bridge-diagnostics';
|
|
64
68
|
import { pinSandboxChannelEventCheckpoint } from '../../utils/sandbox-channel';
|
|
65
69
|
|
|
70
|
+
const invalidToolInputMessage = 'Tool input validation failed.';
|
|
71
|
+
|
|
66
72
|
function unwrapToolResultOutput(toolResult: ToolResultPart): {
|
|
67
73
|
output: unknown;
|
|
68
74
|
isError?: boolean;
|
|
@@ -647,7 +653,9 @@ export function runPrompt<
|
|
|
647
653
|
continuation: ToolApprovalResponse,
|
|
648
654
|
): Promise<'continued' | 'awaiting-tool-result'> => {
|
|
649
655
|
const rawToolCall =
|
|
650
|
-
|
|
656
|
+
(approval.kind === 'builtin'
|
|
657
|
+
? rawToolCallsByToolCallId.get(approval.toolCallId)
|
|
658
|
+
: undefined) ??
|
|
651
659
|
({
|
|
652
660
|
type: 'tool-call',
|
|
653
661
|
toolCallId: approval.toolCallId,
|
|
@@ -656,16 +664,31 @@ export function runPrompt<
|
|
|
656
664
|
providerExecuted: approval.providerExecuted,
|
|
657
665
|
nativeName: approval.nativeName,
|
|
658
666
|
} satisfies Extract<HarnessV1StreamPart, { type: 'tool-call' }>);
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
667
|
+
let validatedToolCall: ToolCallTextStreamPart | undefined;
|
|
668
|
+
let toolCall: ToolCallTextStreamPart;
|
|
669
|
+
if (approval.kind === 'custom' && continuation.approved) {
|
|
670
|
+
validatedToolCall = asToolCallTextStreamPart({
|
|
671
|
+
part: await validateToolCall({
|
|
672
|
+
event: rawToolCall,
|
|
673
|
+
tools: input.tools,
|
|
674
|
+
}),
|
|
675
|
+
});
|
|
676
|
+
toolCall = displayHostToolCall({
|
|
677
|
+
toolCall: validatedToolCall,
|
|
678
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
679
|
+
});
|
|
680
|
+
} else {
|
|
681
|
+
const parsedInput = await safeParseJSON({ text: rawToolCall.input });
|
|
682
|
+
toolCall = {
|
|
683
|
+
type: 'tool-call',
|
|
684
|
+
toolCallId: rawToolCall.toolCallId,
|
|
685
|
+
toolName: rawToolCall.toolName,
|
|
686
|
+
input: parsedInput.success ? parsedInput.value : rawToolCall.input,
|
|
687
|
+
...(rawToolCall.providerExecuted !== undefined
|
|
688
|
+
? { providerExecuted: rawToolCall.providerExecuted }
|
|
689
|
+
: {}),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
669
692
|
|
|
670
693
|
enqueueApprovalResponse(approval, continuation, toolCall);
|
|
671
694
|
onToolApprovalSettled(approval.approvalId);
|
|
@@ -699,6 +722,29 @@ export function runPrompt<
|
|
|
699
722
|
return 'continued';
|
|
700
723
|
}
|
|
701
724
|
|
|
725
|
+
if (validatedToolCall == null) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`Harness '${input.harness.harnessId}' could not validate approved host tool '${approval.toolName}'.`,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (validatedToolCall.invalid) {
|
|
732
|
+
const error = new Error(invalidToolInputMessage);
|
|
733
|
+
await submitToolResult({
|
|
734
|
+
toolCallId: approval.toolCallId,
|
|
735
|
+
output: { error: invalidToolInputMessage },
|
|
736
|
+
isError: true,
|
|
737
|
+
});
|
|
738
|
+
bufferedToolOutcomes.push(() => {
|
|
739
|
+
enqueueHostToolOutcome({
|
|
740
|
+
toolCall,
|
|
741
|
+
outcome: { ok: false, error },
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
await publishToolExecutions();
|
|
745
|
+
return 'continued';
|
|
746
|
+
}
|
|
747
|
+
|
|
702
748
|
await lifecycle.start(input.model);
|
|
703
749
|
toolExecutions.set(rawToolCall.toolCallId, {
|
|
704
750
|
toolCall: toolCall as TypedToolCall<TOOLS>,
|
|
@@ -706,6 +752,7 @@ export function runPrompt<
|
|
|
706
752
|
const executionStartedAt = Date.now();
|
|
707
753
|
const execution = await maybeExecuteHostTool({
|
|
708
754
|
event: rawToolCall,
|
|
755
|
+
parsedToolCall: validatedToolCall,
|
|
709
756
|
tools: activeTools,
|
|
710
757
|
toolsContext,
|
|
711
758
|
wrappedExecuteTool: lifecycle.executeTool,
|
|
@@ -886,7 +933,6 @@ export function runPrompt<
|
|
|
886
933
|
if (settledHostInputReplay || settledBuiltinApprovalReplay) {
|
|
887
934
|
continue;
|
|
888
935
|
}
|
|
889
|
-
|
|
890
936
|
if (displayValue.type === 'finish-step' && closingResumedStep) {
|
|
891
937
|
closingResumedStep = false;
|
|
892
938
|
await publishToolExecutions();
|
|
@@ -971,15 +1017,34 @@ export function runPrompt<
|
|
|
971
1017
|
|
|
972
1018
|
// Tool-call validation lives here (not in translateStreamPart) because
|
|
973
1019
|
// schema parsing is async and needs the merged tool set in scope.
|
|
1020
|
+
let validatedHostToolCall: ToolCallTextStreamPart | undefined;
|
|
974
1021
|
if (displayValue.type === 'tool-call') {
|
|
1022
|
+
const isHostTool =
|
|
1023
|
+
value.type === 'tool-call' &&
|
|
1024
|
+
!value.providerExecuted &&
|
|
1025
|
+
hasTool({ tools: activeTools, toolName: value.toolName }) &&
|
|
1026
|
+
!(
|
|
1027
|
+
value.toolName === 'askUserQuestions' &&
|
|
1028
|
+
hasTool({
|
|
1029
|
+
tools: input.harness.builtinTools,
|
|
1030
|
+
toolName: value.toolName,
|
|
1031
|
+
})
|
|
1032
|
+
);
|
|
975
1033
|
const parsed = await validateToolCall<TOOLS>({
|
|
976
|
-
event: displayValue,
|
|
1034
|
+
event: isHostTool ? value : displayValue,
|
|
977
1035
|
tools: input.tools,
|
|
978
1036
|
});
|
|
979
1037
|
const parsedToolCall = asToolCallTextStreamPart({ part: parsed });
|
|
1038
|
+
validatedHostToolCall = isHostTool ? parsedToolCall : undefined;
|
|
1039
|
+
const displayToolCall = isHostTool
|
|
1040
|
+
? displayHostToolCall({
|
|
1041
|
+
toolCall: parsedToolCall,
|
|
1042
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
1043
|
+
})
|
|
1044
|
+
: parsedToolCall;
|
|
980
1045
|
rawToolCallsByToolCallId.set(displayValue.toolCallId, displayValue);
|
|
981
|
-
toolCallsByToolCallId.set(displayValue.toolCallId,
|
|
982
|
-
result.enqueue(
|
|
1046
|
+
toolCallsByToolCallId.set(displayValue.toolCallId, displayToolCall);
|
|
1047
|
+
result.enqueue(displayToolCall as TextStreamPart<TOOLS>);
|
|
983
1048
|
}
|
|
984
1049
|
|
|
985
1050
|
if (value.type === 'text-delta') {
|
|
@@ -1155,6 +1220,31 @@ export function runPrompt<
|
|
|
1155
1220
|
await finishForHostInputPause({ completeCurrentStep: true });
|
|
1156
1221
|
return;
|
|
1157
1222
|
}
|
|
1223
|
+
if (validatedHostToolCall == null) {
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
`Harness '${input.harness.harnessId}' could not validate host tool '${toolCall.toolName}'.`,
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (validatedHostToolCall.invalid) {
|
|
1229
|
+
settledHostToolCallIds.add(toolCall.toolCallId);
|
|
1230
|
+
toolExecutions.delete(toolCall.toolCallId);
|
|
1231
|
+
bufferedToolOutcomes.push(() => {
|
|
1232
|
+
result.enqueue({
|
|
1233
|
+
type: 'tool-error',
|
|
1234
|
+
toolCallId: toolCall.toolCallId,
|
|
1235
|
+
toolName: toolCall.toolName,
|
|
1236
|
+
input: parsedToolCall.input,
|
|
1237
|
+
error: new Error(invalidToolInputMessage),
|
|
1238
|
+
dynamic: true,
|
|
1239
|
+
} as TextStreamPart<TOOLS>);
|
|
1240
|
+
});
|
|
1241
|
+
await submitToolResult({
|
|
1242
|
+
toolCallId: toolCall.toolCallId,
|
|
1243
|
+
output: { error: invalidToolInputMessage },
|
|
1244
|
+
isError: true,
|
|
1245
|
+
});
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1158
1248
|
const customToolApprovalDecision = resolveCustomToolApproval({
|
|
1159
1249
|
toolName: toolCall.toolName,
|
|
1160
1250
|
toolApproval: input.toolApproval,
|
|
@@ -1266,6 +1356,7 @@ export function runPrompt<
|
|
|
1266
1356
|
const executionStartedAt = Date.now();
|
|
1267
1357
|
const execution = await maybeExecuteHostTool({
|
|
1268
1358
|
event: toolCall,
|
|
1359
|
+
parsedToolCall: validatedHostToolCall,
|
|
1269
1360
|
tools: activeTools,
|
|
1270
1361
|
toolsContext,
|
|
1271
1362
|
wrappedExecuteTool: lifecycle.executeTool,
|
|
@@ -1414,6 +1505,22 @@ function asToolCallTextStreamPart<TOOLS extends ToolSet>(input: {
|
|
|
1414
1505
|
return input.part as ToolCallTextStreamPart;
|
|
1415
1506
|
}
|
|
1416
1507
|
|
|
1508
|
+
function displayHostToolCall(input: {
|
|
1509
|
+
toolCall: ToolCallTextStreamPart;
|
|
1510
|
+
sessionWorkDir: string;
|
|
1511
|
+
}): ToolCallTextStreamPart {
|
|
1512
|
+
return {
|
|
1513
|
+
...input.toolCall,
|
|
1514
|
+
input: stripParsedToolInputWorkDir({
|
|
1515
|
+
value: input.toolCall.input,
|
|
1516
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
1517
|
+
}),
|
|
1518
|
+
...(input.toolCall.invalid
|
|
1519
|
+
? { error: new Error(invalidToolInputMessage) }
|
|
1520
|
+
: {}),
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1417
1524
|
type ToolCallTextStreamPart = {
|
|
1418
1525
|
readonly type: 'tool-call';
|
|
1419
1526
|
readonly toolCallId: string;
|
|
@@ -1433,6 +1540,7 @@ function hasTool(input: { tools: ToolSet; toolName: string }): boolean {
|
|
|
1433
1540
|
|
|
1434
1541
|
async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
1435
1542
|
event: { toolCallId: string; toolName: string; input: string };
|
|
1543
|
+
parsedToolCall: ToolCallTextStreamPart;
|
|
1436
1544
|
tools: TOOLS;
|
|
1437
1545
|
toolsContext: InferToolSetContext<TOOLS>;
|
|
1438
1546
|
wrappedExecuteTool: TurnLifecycle<ToolSet, Context>['executeTool'];
|
|
@@ -1450,8 +1558,15 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
|
1450
1558
|
|
|
1451
1559
|
if (!isExecutableTool(tool)) return { executed: false };
|
|
1452
1560
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1561
|
+
if (input.parsedToolCall.invalid) {
|
|
1562
|
+
const error = new Error(invalidToolInputMessage);
|
|
1563
|
+
await input.submitToolResult({
|
|
1564
|
+
toolCallId: input.event.toolCallId,
|
|
1565
|
+
output: { error: invalidToolInputMessage },
|
|
1566
|
+
isError: true,
|
|
1567
|
+
});
|
|
1568
|
+
return { executed: true, outcome: { ok: false, error } };
|
|
1569
|
+
}
|
|
1455
1570
|
|
|
1456
1571
|
let context: unknown;
|
|
1457
1572
|
try {
|
|
@@ -1489,7 +1604,7 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
|
1489
1604
|
let output: unknown;
|
|
1490
1605
|
const stream = executeTool({
|
|
1491
1606
|
tool,
|
|
1492
|
-
input:
|
|
1607
|
+
input: input.parsedToolCall.input as never,
|
|
1493
1608
|
options: {
|
|
1494
1609
|
toolCallId: input.event.toolCallId,
|
|
1495
1610
|
messages: [],
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
3
|
-
import type
|
|
3
|
+
import { harnessStateDirectoryPath, type HarnessV1Bootstrap } from '../../v1';
|
|
4
|
+
import { encodeHarnessPathSegment } from '../../v1/harness-session-data-directory-path';
|
|
4
5
|
import { resolveSandboxDefaultWorkingDirectory } from '../../utils/resolve-sandbox-default-working-directory';
|
|
6
|
+
import { resolveSandboxHomeDir } from '../../utils/sandbox-home-dir';
|
|
5
7
|
import type { HarnessAgentSandboxConfig } from '../harness-agent-settings';
|
|
6
8
|
import { applyBootstrapRecipe, hashHarnessBootstrap } from './bootstrap-recipe';
|
|
7
9
|
|
|
@@ -78,7 +80,9 @@ export function resolveSessionWorkDir({
|
|
|
78
80
|
}): string {
|
|
79
81
|
return joinSandboxPath({
|
|
80
82
|
base: defaultWorkingDirectory,
|
|
81
|
-
path:
|
|
83
|
+
path:
|
|
84
|
+
workDir ??
|
|
85
|
+
`${encodeHarnessPathSegment(harnessId)}-${encodeHarnessPathSegment(sessionId)}`,
|
|
82
86
|
});
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -147,25 +151,35 @@ export async function runSandboxBootstrap({
|
|
|
147
151
|
}): Promise<void> {
|
|
148
152
|
if (recipe == null && onBootstrap == null) return;
|
|
149
153
|
|
|
150
|
-
const resolvedDefaultWorkingDirectory =
|
|
151
|
-
defaultWorkingDirectory ??
|
|
152
|
-
(await resolveSandboxDefaultWorkingDirectory({
|
|
153
|
-
sandboxSession: session,
|
|
154
|
-
abortSignal,
|
|
155
|
-
}));
|
|
156
|
-
|
|
157
154
|
if (recipe != null && recipeIdentity != null) {
|
|
158
155
|
await applyBootstrapRecipe({
|
|
159
156
|
session,
|
|
160
157
|
recipe,
|
|
161
158
|
identity: recipeIdentity,
|
|
162
|
-
|
|
159
|
+
// Harness infrastructure always lives under the sandbox's own HOME,
|
|
160
|
+
// never the working directory resolved below for the caller's own
|
|
161
|
+
// `onBootstrap` hook. Resolved directly from `session` — this runs
|
|
162
|
+
// from a provider's `onFirstCreate`, before a
|
|
163
|
+
// `HarnessV1NetworkSandboxSession` even exists.
|
|
164
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
165
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
166
|
+
sandbox: session,
|
|
167
|
+
abortSignal,
|
|
168
|
+
}),
|
|
169
|
+
}),
|
|
163
170
|
abortSignal,
|
|
164
171
|
});
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
if (onBootstrap == null) return;
|
|
168
175
|
|
|
176
|
+
const resolvedDefaultWorkingDirectory =
|
|
177
|
+
defaultWorkingDirectory ??
|
|
178
|
+
(await resolveSandboxDefaultWorkingDirectory({
|
|
179
|
+
sandboxSession: session,
|
|
180
|
+
abortSignal,
|
|
181
|
+
}));
|
|
182
|
+
|
|
169
183
|
const bootstrapWorkDir =
|
|
170
184
|
workDir == null
|
|
171
185
|
? resolvedDefaultWorkingDirectory
|
|
@@ -120,6 +120,63 @@ export function stripWorkDir(
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
export function stripParsedToolInputWorkDir({
|
|
124
|
+
value,
|
|
125
|
+
sessionWorkDir,
|
|
126
|
+
}: {
|
|
127
|
+
value: unknown;
|
|
128
|
+
sessionWorkDir: string;
|
|
129
|
+
}): unknown {
|
|
130
|
+
if (sessionWorkDir.length === 0) return value;
|
|
131
|
+
|
|
132
|
+
const projected = new WeakMap<object, unknown>();
|
|
133
|
+
const pending: Array<{
|
|
134
|
+
source: object;
|
|
135
|
+
target: Record<string, unknown> | unknown[];
|
|
136
|
+
}> = [];
|
|
137
|
+
|
|
138
|
+
const project = (item: unknown): unknown => {
|
|
139
|
+
if (typeof item === 'string') return stripString(item, sessionWorkDir);
|
|
140
|
+
if (item === null || typeof item !== 'object') return item;
|
|
141
|
+
if (
|
|
142
|
+
!Array.isArray(item) &&
|
|
143
|
+
Object.getPrototypeOf(item) !== Object.prototype &&
|
|
144
|
+
Object.getPrototypeOf(item) !== null
|
|
145
|
+
) {
|
|
146
|
+
return item;
|
|
147
|
+
}
|
|
148
|
+
if (projected.has(item)) return projected.get(item);
|
|
149
|
+
|
|
150
|
+
const target: Record<string, unknown> | unknown[] = Array.isArray(item)
|
|
151
|
+
? new Array(item.length)
|
|
152
|
+
: {};
|
|
153
|
+
projected.set(item, target);
|
|
154
|
+
pending.push({ source: item, target });
|
|
155
|
+
return target;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const result = project(value);
|
|
159
|
+
while (pending.length > 0) {
|
|
160
|
+
const { source, target } = pending.pop()!;
|
|
161
|
+
if (Array.isArray(source) && Array.isArray(target)) {
|
|
162
|
+
for (let index = 0; index < source.length; index++) {
|
|
163
|
+
if (index in source) target[index] = project(source[index]);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
for (const [key, item] of Object.entries(source)) {
|
|
167
|
+
Object.defineProperty(target, key, {
|
|
168
|
+
value: project(item),
|
|
169
|
+
enumerable: true,
|
|
170
|
+
writable: true,
|
|
171
|
+
configurable: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
123
180
|
/**
|
|
124
181
|
* Replace occurrences of the working directory in a string. A reference to the
|
|
125
182
|
* directory followed by a separator becomes workspace-relative
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
harnessStateDirectoryPath,
|
|
3
|
+
type HarnessV1Bootstrap,
|
|
4
|
+
type HarnessV1SandboxProvider,
|
|
5
|
+
} from '../v1';
|
|
6
|
+
import { resolveSandboxHomeDir } from '../utils/sandbox-home-dir';
|
|
2
7
|
import type { HarnessAgentAdapter } from './harness-agent-types';
|
|
3
8
|
import type { HarnessAgentSandboxConfig } from './harness-agent-settings';
|
|
4
9
|
import { applyBootstrapRecipe } from './internal/bootstrap-recipe';
|
|
@@ -58,11 +63,19 @@ export async function prepareHarnessSandboxTemplate(options: {
|
|
|
58
63
|
// general harness utility, not for a concrete agent.
|
|
59
64
|
try {
|
|
60
65
|
if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
|
|
66
|
+
const restrictedSession = sandboxSession.restricted();
|
|
61
67
|
await applyBootstrapRecipe({
|
|
62
|
-
session:
|
|
68
|
+
session: restrictedSession,
|
|
63
69
|
recipe: bootstrapPlan.recipe,
|
|
64
70
|
identity: bootstrapPlan.recipeIdentity,
|
|
65
|
-
|
|
71
|
+
// Harness infrastructure always lives under the sandbox's own HOME,
|
|
72
|
+
// never the working directory.
|
|
73
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
74
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
75
|
+
sandbox: restrictedSession,
|
|
76
|
+
abortSignal,
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
66
79
|
abortSignal,
|
|
67
80
|
});
|
|
68
81
|
}
|