@ai-sdk/harness 1.0.52 → 1.0.54
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 +16 -0
- package/README.md +9 -0
- package/dist/agent/index.d.ts +14 -6
- package/dist/agent/index.js +102 -37
- package/dist/agent/index.js.map +1 -1
- package/dist/index.d.ts +14 -6
- package/package.json +4 -4
- package/src/agent/harness-agent.ts +7 -6
- package/src/agent/internal/bootstrap-recipe.ts +83 -23
- package/src/agent/internal/sandbox-bootstrap.ts +18 -7
- package/src/agent/prepare-harness-sandbox-template.ts +7 -8
- package/src/agent/prepare-sandbox-for-harness.ts +12 -1
- package/src/v1/harness-v1-bootstrap.ts +14 -6
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { JSONValue, LanguageModelV4ToolCall, LanguageModelV4ToolApprovalRequest,
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* One file to write into the sandbox as part of an adapter's bootstrap recipe.
|
|
9
|
-
*
|
|
9
|
+
* Absolute paths are used as-is. Relative paths are resolved against the
|
|
10
|
+
* sandbox's default working directory. Paths should live under
|
|
11
|
+
* {@link HarnessV1Bootstrap.bootstrapDir}.
|
|
10
12
|
*/
|
|
11
13
|
interface HarnessV1BootstrapFile {
|
|
12
14
|
readonly path: string;
|
|
@@ -19,6 +21,11 @@ interface HarnessV1BootstrapFile {
|
|
|
19
21
|
*/
|
|
20
22
|
interface HarnessV1BootstrapCommand {
|
|
21
23
|
readonly command: string;
|
|
24
|
+
/**
|
|
25
|
+
* Absolute working directories are used as-is. Relative working directories
|
|
26
|
+
* are resolved against the sandbox's default working directory. When
|
|
27
|
+
* omitted, the command runs from the recipe's bootstrap directory.
|
|
28
|
+
*/
|
|
22
29
|
readonly workingDirectory?: string;
|
|
23
30
|
}
|
|
24
31
|
/**
|
|
@@ -34,15 +41,16 @@ interface HarnessV1Bootstrap {
|
|
|
34
41
|
*/
|
|
35
42
|
readonly harnessId: string;
|
|
36
43
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
44
|
+
* Path inside the sandbox where this recipe writes its state. Absolute paths
|
|
45
|
+
* are used as-is. Relative paths are resolved against the sandbox's default
|
|
46
|
+
* working directory. The marker file lives directly under it. Files
|
|
47
|
+
* declared in {@link files} should also use this prefix so an adapter upgrade
|
|
48
|
+
* can sweep stale state by clearing the directory.
|
|
41
49
|
*/
|
|
42
50
|
readonly bootstrapDir: string;
|
|
43
51
|
/** Files to write into the sandbox before any command runs. */
|
|
44
52
|
readonly files: ReadonlyArray<HarnessV1BootstrapFile>;
|
|
45
|
-
/** Commands to run after files are written
|
|
53
|
+
/** Commands to run from the bootstrap directory after files are written. */
|
|
46
54
|
readonly commands: ReadonlyArray<HarnessV1BootstrapCommand>;
|
|
47
55
|
}
|
|
48
56
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.54",
|
|
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.4",
|
|
48
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
49
|
-
"ai": "7.0.
|
|
48
|
+
"@ai-sdk/provider-utils": "5.0.18",
|
|
49
|
+
"ai": "7.0.48"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"ws": "^8.21.0",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"typescript": "5.8.3",
|
|
66
66
|
"ws": "^8.21.0",
|
|
67
67
|
"zod": "3.25.76",
|
|
68
|
-
"@ai-sdk/otel": "1.0.
|
|
68
|
+
"@ai-sdk/otel": "1.0.48",
|
|
69
69
|
"@vercel/ai-tsconfig": "0.0.0"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
@@ -310,12 +310,13 @@ export class HarnessAgent<
|
|
|
310
310
|
sandboxBootstrapPlan.recipe != null &&
|
|
311
311
|
sandboxBootstrapPlan.recipeIdentity != null
|
|
312
312
|
) {
|
|
313
|
-
await applyBootstrapRecipe(
|
|
314
|
-
sandboxSession.restricted(),
|
|
315
|
-
sandboxBootstrapPlan.recipe,
|
|
316
|
-
sandboxBootstrapPlan.recipeIdentity,
|
|
317
|
-
|
|
318
|
-
|
|
313
|
+
await applyBootstrapRecipe({
|
|
314
|
+
session: sandboxSession.restricted(),
|
|
315
|
+
recipe: sandboxBootstrapPlan.recipe,
|
|
316
|
+
identity: sandboxBootstrapPlan.recipeIdentity,
|
|
317
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
318
|
+
abortSignal,
|
|
319
|
+
});
|
|
319
320
|
}
|
|
320
321
|
await ensureSandboxDirectory({
|
|
321
322
|
session: sandboxSession,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
1
2
|
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
3
|
import type { HarnessV1Bootstrap } from '../../v1';
|
|
3
4
|
|
|
@@ -59,55 +60,102 @@ export async function hashHarnessBootstrap(
|
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* Absolute path of the marker file the framework writes after a recipe runs
|
|
62
|
-
* successfully. Presence of this path inside the sandbox indicates the
|
|
63
|
-
*
|
|
63
|
+
* successfully. Presence of this path inside the sandbox indicates the recipe
|
|
64
|
+
* with the matching `identity` has already been applied.
|
|
64
65
|
*/
|
|
65
|
-
export function bootstrapMarkerPath(
|
|
66
|
-
recipe
|
|
67
|
-
identity
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
export function bootstrapMarkerPath({
|
|
67
|
+
recipe,
|
|
68
|
+
identity,
|
|
69
|
+
defaultWorkingDirectory,
|
|
70
|
+
}: {
|
|
71
|
+
recipe: HarnessV1Bootstrap;
|
|
72
|
+
identity: string;
|
|
73
|
+
defaultWorkingDirectory: string;
|
|
74
|
+
}): string {
|
|
75
|
+
return posix.join(
|
|
76
|
+
resolveBootstrapPath({
|
|
77
|
+
path: recipe.bootstrapDir,
|
|
78
|
+
defaultWorkingDirectory,
|
|
79
|
+
}),
|
|
80
|
+
`.bootstrap-${identity}.ok`,
|
|
81
|
+
);
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
/**
|
|
73
85
|
* Apply a bootstrap recipe to a sandbox session idempotently. Reads the
|
|
74
|
-
* marker file; if it exists, returns immediately. Otherwise
|
|
75
|
-
* recipe's files, runs its commands
|
|
76
|
-
* on success.
|
|
86
|
+
* marker file; if it exists, returns immediately. Otherwise creates the
|
|
87
|
+
* bootstrap directory, writes the recipe's files, runs its commands
|
|
88
|
+
* sequentially, and writes the marker on success.
|
|
77
89
|
*
|
|
78
90
|
* Safe to call multiple times. For sandboxes that already contain the
|
|
79
91
|
* recipe (resumed from snapshot, reused across sessions, or applied by
|
|
80
92
|
* an earlier process) this is a single fast read.
|
|
81
93
|
*/
|
|
82
|
-
export async function applyBootstrapRecipe(
|
|
83
|
-
session
|
|
84
|
-
recipe
|
|
85
|
-
identity
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
export async function applyBootstrapRecipe({
|
|
95
|
+
session,
|
|
96
|
+
recipe,
|
|
97
|
+
identity,
|
|
98
|
+
defaultWorkingDirectory,
|
|
99
|
+
abortSignal,
|
|
100
|
+
}: {
|
|
101
|
+
session: SandboxSession;
|
|
102
|
+
recipe: HarnessV1Bootstrap;
|
|
103
|
+
identity: string;
|
|
104
|
+
defaultWorkingDirectory: string;
|
|
105
|
+
abortSignal?: AbortSignal;
|
|
106
|
+
}): Promise<void> {
|
|
107
|
+
const markerPath = bootstrapMarkerPath({
|
|
108
|
+
recipe,
|
|
109
|
+
identity,
|
|
110
|
+
defaultWorkingDirectory,
|
|
111
|
+
});
|
|
89
112
|
|
|
90
113
|
const existingMarker = await session.readTextFile({
|
|
91
114
|
path: markerPath,
|
|
92
|
-
abortSignal
|
|
115
|
+
abortSignal,
|
|
93
116
|
});
|
|
94
117
|
if (existingMarker !== null) {
|
|
95
118
|
return;
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
const bootstrapDir = resolveBootstrapPath({
|
|
122
|
+
path: recipe.bootstrapDir,
|
|
123
|
+
defaultWorkingDirectory,
|
|
124
|
+
});
|
|
125
|
+
const mkdirResult = await session.run({
|
|
126
|
+
command: 'mkdir -p "$BOOTSTRAP_DIR"',
|
|
127
|
+
workingDirectory: defaultWorkingDirectory,
|
|
128
|
+
env: { BOOTSTRAP_DIR: bootstrapDir },
|
|
129
|
+
abortSignal,
|
|
130
|
+
});
|
|
131
|
+
if (mkdirResult.exitCode !== 0) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Failed to create bootstrap directory for harness '${recipe.harnessId}' (exit ${mkdirResult.exitCode}): ${bootstrapDir}\n${mkdirResult.stderr || mkdirResult.stdout}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
98
137
|
for (const file of recipe.files) {
|
|
99
138
|
await session.writeTextFile({
|
|
100
|
-
path:
|
|
139
|
+
path: resolveBootstrapPath({
|
|
140
|
+
path: file.path,
|
|
141
|
+
defaultWorkingDirectory,
|
|
142
|
+
}),
|
|
101
143
|
content: file.content,
|
|
102
|
-
abortSignal
|
|
144
|
+
abortSignal,
|
|
103
145
|
});
|
|
104
146
|
}
|
|
105
147
|
|
|
106
148
|
for (const cmd of recipe.commands) {
|
|
107
149
|
const result = await session.run({
|
|
108
150
|
command: cmd.command,
|
|
109
|
-
workingDirectory:
|
|
110
|
-
|
|
151
|
+
workingDirectory:
|
|
152
|
+
cmd.workingDirectory == null
|
|
153
|
+
? bootstrapDir
|
|
154
|
+
: resolveBootstrapPath({
|
|
155
|
+
path: cmd.workingDirectory,
|
|
156
|
+
defaultWorkingDirectory,
|
|
157
|
+
}),
|
|
158
|
+
abortSignal,
|
|
111
159
|
});
|
|
112
160
|
if (result.exitCode !== 0) {
|
|
113
161
|
throw new Error(
|
|
@@ -119,6 +167,18 @@ export async function applyBootstrapRecipe(
|
|
|
119
167
|
await session.writeTextFile({
|
|
120
168
|
path: markerPath,
|
|
121
169
|
content: '',
|
|
122
|
-
abortSignal
|
|
170
|
+
abortSignal,
|
|
123
171
|
});
|
|
124
172
|
}
|
|
173
|
+
|
|
174
|
+
function resolveBootstrapPath({
|
|
175
|
+
path,
|
|
176
|
+
defaultWorkingDirectory,
|
|
177
|
+
}: {
|
|
178
|
+
path: string;
|
|
179
|
+
defaultWorkingDirectory: string;
|
|
180
|
+
}): string {
|
|
181
|
+
return posix.isAbsolute(path)
|
|
182
|
+
? path
|
|
183
|
+
: posix.resolve(defaultWorkingDirectory, path);
|
|
184
|
+
}
|
|
@@ -133,6 +133,7 @@ export async function runSandboxBootstrap({
|
|
|
133
133
|
recipeIdentity,
|
|
134
134
|
workDir,
|
|
135
135
|
onBootstrap,
|
|
136
|
+
defaultWorkingDirectory,
|
|
136
137
|
abortSignal,
|
|
137
138
|
}: {
|
|
138
139
|
readonly session: SandboxSession;
|
|
@@ -140,25 +141,35 @@ export async function runSandboxBootstrap({
|
|
|
140
141
|
readonly recipeIdentity?: string;
|
|
141
142
|
readonly workDir?: string;
|
|
142
143
|
readonly onBootstrap?: SandboxBootstrapSettings['onBootstrap'];
|
|
144
|
+
readonly defaultWorkingDirectory?: string;
|
|
143
145
|
readonly abortSignal?: AbortSignal;
|
|
144
146
|
}): Promise<void> {
|
|
147
|
+
if (recipe == null && onBootstrap == null) return;
|
|
148
|
+
|
|
149
|
+
const resolvedDefaultWorkingDirectory =
|
|
150
|
+
defaultWorkingDirectory ??
|
|
151
|
+
(await resolveDefaultWorkingDirectory({
|
|
152
|
+
session,
|
|
153
|
+
abortSignal,
|
|
154
|
+
}));
|
|
155
|
+
|
|
145
156
|
if (recipe != null && recipeIdentity != null) {
|
|
146
|
-
await applyBootstrapRecipe(
|
|
157
|
+
await applyBootstrapRecipe({
|
|
158
|
+
session,
|
|
159
|
+
recipe,
|
|
160
|
+
identity: recipeIdentity,
|
|
161
|
+
defaultWorkingDirectory: resolvedDefaultWorkingDirectory,
|
|
147
162
|
abortSignal,
|
|
148
163
|
});
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
if (onBootstrap == null) return;
|
|
152
167
|
|
|
153
|
-
const defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
|
|
154
|
-
session,
|
|
155
|
-
abortSignal,
|
|
156
|
-
});
|
|
157
168
|
const bootstrapWorkDir =
|
|
158
169
|
workDir == null
|
|
159
|
-
?
|
|
170
|
+
? resolvedDefaultWorkingDirectory
|
|
160
171
|
: joinSandboxPath({
|
|
161
|
-
base:
|
|
172
|
+
base: resolvedDefaultWorkingDirectory,
|
|
162
173
|
path: workDir,
|
|
163
174
|
});
|
|
164
175
|
|
|
@@ -50,14 +50,13 @@ export async function prepareHarnessSandboxTemplate(options: {
|
|
|
50
50
|
|
|
51
51
|
try {
|
|
52
52
|
if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
|
|
53
|
-
await applyBootstrapRecipe(
|
|
54
|
-
sandboxSession.restricted(),
|
|
55
|
-
bootstrapPlan.recipe,
|
|
56
|
-
bootstrapPlan.recipeIdentity,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
53
|
+
await applyBootstrapRecipe({
|
|
54
|
+
session: sandboxSession.restricted(),
|
|
55
|
+
recipe: bootstrapPlan.recipe,
|
|
56
|
+
identity: bootstrapPlan.recipeIdentity,
|
|
57
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
58
|
+
abortSignal: options.abortSignal,
|
|
59
|
+
});
|
|
61
60
|
}
|
|
62
61
|
} finally {
|
|
63
62
|
await Promise.resolve(sandboxSession.stop()).catch(() => {});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './internal/bootstrap-recipe';
|
|
8
8
|
import {
|
|
9
9
|
normalizeSandboxWorkDir,
|
|
10
|
+
resolveDefaultWorkingDirectory,
|
|
10
11
|
runSandboxBootstrap,
|
|
11
12
|
validateSandboxBootstrapSettings,
|
|
12
13
|
} from './internal/sandbox-bootstrap';
|
|
@@ -62,6 +63,7 @@ export async function prepareSandboxForHarness(options: {
|
|
|
62
63
|
: normalizeSandboxWorkDir(sandboxConfig.workDir);
|
|
63
64
|
const recipeIdentities: Record<string, string> = {};
|
|
64
65
|
const skippedHarnessIds: string[] = [];
|
|
66
|
+
let defaultWorkingDirectory: string | undefined;
|
|
65
67
|
|
|
66
68
|
for (const harness of harnesses) {
|
|
67
69
|
const recipe = await harness.getBootstrap?.({
|
|
@@ -74,7 +76,15 @@ export async function prepareSandboxForHarness(options: {
|
|
|
74
76
|
|
|
75
77
|
const recipeIdentity = await hashHarnessBootstrap(recipe);
|
|
76
78
|
recipeIdentities[harness.harnessId] = recipeIdentity;
|
|
77
|
-
await
|
|
79
|
+
defaultWorkingDirectory ??= await resolveDefaultWorkingDirectory({
|
|
80
|
+
session: options.session,
|
|
81
|
+
abortSignal: options.abortSignal,
|
|
82
|
+
});
|
|
83
|
+
await applyBootstrapRecipe({
|
|
84
|
+
session: options.session,
|
|
85
|
+
recipe,
|
|
86
|
+
identity: recipeIdentity,
|
|
87
|
+
defaultWorkingDirectory,
|
|
78
88
|
abortSignal: options.abortSignal,
|
|
79
89
|
});
|
|
80
90
|
}
|
|
@@ -84,6 +94,7 @@ export async function prepareSandboxForHarness(options: {
|
|
|
84
94
|
session: options.session,
|
|
85
95
|
workDir,
|
|
86
96
|
onBootstrap: sandboxConfig.onBootstrap,
|
|
97
|
+
defaultWorkingDirectory,
|
|
87
98
|
abortSignal: options.abortSignal,
|
|
88
99
|
});
|
|
89
100
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One file to write into the sandbox as part of an adapter's bootstrap recipe.
|
|
3
|
-
*
|
|
3
|
+
* Absolute paths are used as-is. Relative paths are resolved against the
|
|
4
|
+
* sandbox's default working directory. Paths should live under
|
|
5
|
+
* {@link HarnessV1Bootstrap.bootstrapDir}.
|
|
4
6
|
*/
|
|
5
7
|
export interface HarnessV1BootstrapFile {
|
|
6
8
|
readonly path: string;
|
|
@@ -14,6 +16,11 @@ export interface HarnessV1BootstrapFile {
|
|
|
14
16
|
*/
|
|
15
17
|
export interface HarnessV1BootstrapCommand {
|
|
16
18
|
readonly command: string;
|
|
19
|
+
/**
|
|
20
|
+
* Absolute working directories are used as-is. Relative working directories
|
|
21
|
+
* are resolved against the sandbox's default working directory. When
|
|
22
|
+
* omitted, the command runs from the recipe's bootstrap directory.
|
|
23
|
+
*/
|
|
17
24
|
readonly workingDirectory?: string;
|
|
18
25
|
}
|
|
19
26
|
|
|
@@ -31,16 +38,17 @@ export interface HarnessV1Bootstrap {
|
|
|
31
38
|
readonly harnessId: string;
|
|
32
39
|
|
|
33
40
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
41
|
+
* Path inside the sandbox where this recipe writes its state. Absolute paths
|
|
42
|
+
* are used as-is. Relative paths are resolved against the sandbox's default
|
|
43
|
+
* working directory. The marker file lives directly under it. Files
|
|
44
|
+
* declared in {@link files} should also use this prefix so an adapter upgrade
|
|
45
|
+
* can sweep stale state by clearing the directory.
|
|
38
46
|
*/
|
|
39
47
|
readonly bootstrapDir: string;
|
|
40
48
|
|
|
41
49
|
/** Files to write into the sandbox before any command runs. */
|
|
42
50
|
readonly files: ReadonlyArray<HarnessV1BootstrapFile>;
|
|
43
51
|
|
|
44
|
-
/** Commands to run after files are written
|
|
52
|
+
/** Commands to run from the bootstrap directory after files are written. */
|
|
45
53
|
readonly commands: ReadonlyArray<HarnessV1BootstrapCommand>;
|
|
46
54
|
}
|