@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @ai-sdk/harness
|
|
2
2
|
|
|
3
|
+
## 1.0.54
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ai@7.0.48
|
|
8
|
+
|
|
9
|
+
## 1.0.53
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- bdde5d9: fix(harness): avoid placing harness bootstrap files in `/tmp` and instead use sandbox working directory
|
|
14
|
+
- Updated dependencies [5fc7da5]
|
|
15
|
+
- Updated dependencies [93b2acd]
|
|
16
|
+
- @ai-sdk/provider-utils@5.0.18
|
|
17
|
+
- ai@7.0.47
|
|
18
|
+
|
|
3
19
|
## 1.0.52
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -111,6 +111,15 @@ See the [harness adapters documentation](https://ai-sdk.dev/v7/docs/ai-sdk-harne
|
|
|
111
111
|
|
|
112
112
|
Implement the `HarnessV1` factory and a `HarnessV1Session` whose `doPromptTurn` emits events; the agent surface, streaming, tool execution, and multi-turn state are handled for you. Read `startOpts.sandboxSession` for the network sandbox session the agent created and will stop on cleanup. Call `sandboxSession.restricted()` for the tool-safe file-IO/exec/spawn surface.
|
|
113
113
|
|
|
114
|
+
Bootstrap recipe paths may be absolute or relative. Relative `bootstrapDir` and
|
|
115
|
+
file paths are resolved against `sandboxSession.defaultWorkingDirectory`.
|
|
116
|
+
The framework creates `bootstrapDir` before writing files, and bootstrap
|
|
117
|
+
commands run from that directory by default. A relative command
|
|
118
|
+
`workingDirectory` override is resolved against the sandbox's default working
|
|
119
|
+
directory. Prefer a relative directory such as
|
|
120
|
+
`.harness-bootstrap/my-harness` so bootstrap assets are kept with the sandbox's
|
|
121
|
+
snapshot-persistent working tree.
|
|
122
|
+
|
|
114
123
|
```ts
|
|
115
124
|
import type { HarnessV1, HarnessV1Session } from '@ai-sdk/harness';
|
|
116
125
|
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -75,7 +75,9 @@ interface HarnessDiagnosticConsumer {
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* One file to write into the sandbox as part of an adapter's bootstrap recipe.
|
|
78
|
-
*
|
|
78
|
+
* Absolute paths are used as-is. Relative paths are resolved against the
|
|
79
|
+
* sandbox's default working directory. Paths should live under
|
|
80
|
+
* {@link HarnessV1Bootstrap.bootstrapDir}.
|
|
79
81
|
*/
|
|
80
82
|
interface HarnessV1BootstrapFile {
|
|
81
83
|
readonly path: string;
|
|
@@ -88,6 +90,11 @@ interface HarnessV1BootstrapFile {
|
|
|
88
90
|
*/
|
|
89
91
|
interface HarnessV1BootstrapCommand {
|
|
90
92
|
readonly command: string;
|
|
93
|
+
/**
|
|
94
|
+
* Absolute working directories are used as-is. Relative working directories
|
|
95
|
+
* are resolved against the sandbox's default working directory. When
|
|
96
|
+
* omitted, the command runs from the recipe's bootstrap directory.
|
|
97
|
+
*/
|
|
91
98
|
readonly workingDirectory?: string;
|
|
92
99
|
}
|
|
93
100
|
/**
|
|
@@ -103,15 +110,16 @@ interface HarnessV1Bootstrap {
|
|
|
103
110
|
*/
|
|
104
111
|
readonly harnessId: string;
|
|
105
112
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
113
|
+
* Path inside the sandbox where this recipe writes its state. Absolute paths
|
|
114
|
+
* are used as-is. Relative paths are resolved against the sandbox's default
|
|
115
|
+
* working directory. The marker file lives directly under it. Files
|
|
116
|
+
* declared in {@link files} should also use this prefix so an adapter upgrade
|
|
117
|
+
* can sweep stale state by clearing the directory.
|
|
110
118
|
*/
|
|
111
119
|
readonly bootstrapDir: string;
|
|
112
120
|
/** Files to write into the sandbox before any command runs. */
|
|
113
121
|
readonly files: ReadonlyArray<HarnessV1BootstrapFile>;
|
|
114
|
-
/** Commands to run after files are written
|
|
122
|
+
/** Commands to run from the bootstrap directory after files are written. */
|
|
115
123
|
readonly commands: ReadonlyArray<HarnessV1BootstrapCommand>;
|
|
116
124
|
}
|
|
117
125
|
|
package/dist/agent/index.js
CHANGED
|
@@ -2683,6 +2683,7 @@ function toToolResultContinuation(input) {
|
|
|
2683
2683
|
}
|
|
2684
2684
|
|
|
2685
2685
|
// src/agent/internal/bootstrap-recipe.ts
|
|
2686
|
+
import { posix } from "path";
|
|
2686
2687
|
var BOOTSTRAP_SCHEMA_VERSION = 1;
|
|
2687
2688
|
async function hashHarnessBootstrap(recipe) {
|
|
2688
2689
|
const encoder = new TextEncoder();
|
|
@@ -2717,30 +2718,72 @@ async function hashHarnessBootstrap(recipe) {
|
|
|
2717
2718
|
}
|
|
2718
2719
|
return hex;
|
|
2719
2720
|
}
|
|
2720
|
-
function bootstrapMarkerPath(
|
|
2721
|
-
|
|
2721
|
+
function bootstrapMarkerPath({
|
|
2722
|
+
recipe,
|
|
2723
|
+
identity,
|
|
2724
|
+
defaultWorkingDirectory
|
|
2725
|
+
}) {
|
|
2726
|
+
return posix.join(
|
|
2727
|
+
resolveBootstrapPath({
|
|
2728
|
+
path: recipe.bootstrapDir,
|
|
2729
|
+
defaultWorkingDirectory
|
|
2730
|
+
}),
|
|
2731
|
+
`.bootstrap-${identity}.ok`
|
|
2732
|
+
);
|
|
2722
2733
|
}
|
|
2723
|
-
async function applyBootstrapRecipe(
|
|
2724
|
-
|
|
2734
|
+
async function applyBootstrapRecipe({
|
|
2735
|
+
session,
|
|
2736
|
+
recipe,
|
|
2737
|
+
identity,
|
|
2738
|
+
defaultWorkingDirectory,
|
|
2739
|
+
abortSignal
|
|
2740
|
+
}) {
|
|
2741
|
+
const markerPath = bootstrapMarkerPath({
|
|
2742
|
+
recipe,
|
|
2743
|
+
identity,
|
|
2744
|
+
defaultWorkingDirectory
|
|
2745
|
+
});
|
|
2725
2746
|
const existingMarker = await session.readTextFile({
|
|
2726
2747
|
path: markerPath,
|
|
2727
|
-
abortSignal
|
|
2748
|
+
abortSignal
|
|
2728
2749
|
});
|
|
2729
2750
|
if (existingMarker !== null) {
|
|
2730
2751
|
return;
|
|
2731
2752
|
}
|
|
2753
|
+
const bootstrapDir = resolveBootstrapPath({
|
|
2754
|
+
path: recipe.bootstrapDir,
|
|
2755
|
+
defaultWorkingDirectory
|
|
2756
|
+
});
|
|
2757
|
+
const mkdirResult = await session.run({
|
|
2758
|
+
command: 'mkdir -p "$BOOTSTRAP_DIR"',
|
|
2759
|
+
workingDirectory: defaultWorkingDirectory,
|
|
2760
|
+
env: { BOOTSTRAP_DIR: bootstrapDir },
|
|
2761
|
+
abortSignal
|
|
2762
|
+
});
|
|
2763
|
+
if (mkdirResult.exitCode !== 0) {
|
|
2764
|
+
throw new Error(
|
|
2765
|
+
`Failed to create bootstrap directory for harness '${recipe.harnessId}' (exit ${mkdirResult.exitCode}): ${bootstrapDir}
|
|
2766
|
+
${mkdirResult.stderr || mkdirResult.stdout}`
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2732
2769
|
for (const file of recipe.files) {
|
|
2733
2770
|
await session.writeTextFile({
|
|
2734
|
-
path:
|
|
2771
|
+
path: resolveBootstrapPath({
|
|
2772
|
+
path: file.path,
|
|
2773
|
+
defaultWorkingDirectory
|
|
2774
|
+
}),
|
|
2735
2775
|
content: file.content,
|
|
2736
|
-
abortSignal
|
|
2776
|
+
abortSignal
|
|
2737
2777
|
});
|
|
2738
2778
|
}
|
|
2739
2779
|
for (const cmd of recipe.commands) {
|
|
2740
2780
|
const result = await session.run({
|
|
2741
2781
|
command: cmd.command,
|
|
2742
|
-
workingDirectory: cmd.workingDirectory
|
|
2743
|
-
|
|
2782
|
+
workingDirectory: cmd.workingDirectory == null ? bootstrapDir : resolveBootstrapPath({
|
|
2783
|
+
path: cmd.workingDirectory,
|
|
2784
|
+
defaultWorkingDirectory
|
|
2785
|
+
}),
|
|
2786
|
+
abortSignal
|
|
2744
2787
|
});
|
|
2745
2788
|
if (result.exitCode !== 0) {
|
|
2746
2789
|
throw new Error(
|
|
@@ -2752,12 +2795,18 @@ ${result.stderr || result.stdout}`
|
|
|
2752
2795
|
await session.writeTextFile({
|
|
2753
2796
|
path: markerPath,
|
|
2754
2797
|
content: "",
|
|
2755
|
-
abortSignal
|
|
2798
|
+
abortSignal
|
|
2756
2799
|
});
|
|
2757
2800
|
}
|
|
2801
|
+
function resolveBootstrapPath({
|
|
2802
|
+
path,
|
|
2803
|
+
defaultWorkingDirectory
|
|
2804
|
+
}) {
|
|
2805
|
+
return posix.isAbsolute(path) ? path : posix.resolve(defaultWorkingDirectory, path);
|
|
2806
|
+
}
|
|
2758
2807
|
|
|
2759
2808
|
// src/agent/internal/sandbox-bootstrap.ts
|
|
2760
|
-
import { posix } from "path";
|
|
2809
|
+
import { posix as posix2 } from "path";
|
|
2761
2810
|
var SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
|
|
2762
2811
|
function validateSandboxBootstrapSettings(settings) {
|
|
2763
2812
|
if (settings.onBootstrap == null !== (settings.bootstrapHash == null)) {
|
|
@@ -2783,10 +2832,10 @@ function normalizeSandboxWorkDir(workDir) {
|
|
|
2783
2832
|
"HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators."
|
|
2784
2833
|
);
|
|
2785
2834
|
}
|
|
2786
|
-
if (
|
|
2835
|
+
if (posix2.isAbsolute(workDir)) {
|
|
2787
2836
|
throw new Error("HarnessAgent: `sandboxConfig.workDir` must be relative.");
|
|
2788
2837
|
}
|
|
2789
|
-
const normalized =
|
|
2838
|
+
const normalized = posix2.normalize(workDir);
|
|
2790
2839
|
if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
2791
2840
|
throw new Error(
|
|
2792
2841
|
"HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory."
|
|
@@ -2841,20 +2890,26 @@ async function runSandboxBootstrap({
|
|
|
2841
2890
|
recipeIdentity,
|
|
2842
2891
|
workDir,
|
|
2843
2892
|
onBootstrap,
|
|
2893
|
+
defaultWorkingDirectory,
|
|
2844
2894
|
abortSignal
|
|
2845
2895
|
}) {
|
|
2896
|
+
if (recipe == null && onBootstrap == null) return;
|
|
2897
|
+
const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveDefaultWorkingDirectory({
|
|
2898
|
+
session,
|
|
2899
|
+
abortSignal
|
|
2900
|
+
});
|
|
2846
2901
|
if (recipe != null && recipeIdentity != null) {
|
|
2847
|
-
await applyBootstrapRecipe(
|
|
2902
|
+
await applyBootstrapRecipe({
|
|
2903
|
+
session,
|
|
2904
|
+
recipe,
|
|
2905
|
+
identity: recipeIdentity,
|
|
2906
|
+
defaultWorkingDirectory: resolvedDefaultWorkingDirectory,
|
|
2848
2907
|
abortSignal
|
|
2849
2908
|
});
|
|
2850
2909
|
}
|
|
2851
2910
|
if (onBootstrap == null) return;
|
|
2852
|
-
const
|
|
2853
|
-
|
|
2854
|
-
abortSignal
|
|
2855
|
-
});
|
|
2856
|
-
const bootstrapWorkDir = workDir == null ? defaultWorkingDirectory : joinSandboxPath({
|
|
2857
|
-
base: defaultWorkingDirectory,
|
|
2911
|
+
const bootstrapWorkDir = workDir == null ? resolvedDefaultWorkingDirectory : joinSandboxPath({
|
|
2912
|
+
base: resolvedDefaultWorkingDirectory,
|
|
2858
2913
|
path: workDir
|
|
2859
2914
|
});
|
|
2860
2915
|
await ensureSandboxDirectory({
|
|
@@ -2878,7 +2933,7 @@ async function resolveDefaultWorkingDirectory({
|
|
|
2878
2933
|
);
|
|
2879
2934
|
}
|
|
2880
2935
|
const cwd = result.stdout.trim();
|
|
2881
|
-
if (!
|
|
2936
|
+
if (!posix2.isAbsolute(cwd)) {
|
|
2882
2937
|
throw new Error(
|
|
2883
2938
|
`Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
|
|
2884
2939
|
);
|
|
@@ -2935,7 +2990,7 @@ function joinSandboxPath({
|
|
|
2935
2990
|
base,
|
|
2936
2991
|
path
|
|
2937
2992
|
}) {
|
|
2938
|
-
return
|
|
2993
|
+
return posix2.join(base, path);
|
|
2939
2994
|
}
|
|
2940
2995
|
|
|
2941
2996
|
// src/agent/internal/resolve-observability.ts
|
|
@@ -3180,12 +3235,13 @@ var HarnessAgent = class {
|
|
|
3180
3235
|
});
|
|
3181
3236
|
try {
|
|
3182
3237
|
if (!isResumedSession && sandboxBootstrapPlan.recipe != null && sandboxBootstrapPlan.recipeIdentity != null) {
|
|
3183
|
-
await applyBootstrapRecipe(
|
|
3184
|
-
sandboxSession.restricted(),
|
|
3185
|
-
sandboxBootstrapPlan.recipe,
|
|
3186
|
-
sandboxBootstrapPlan.recipeIdentity,
|
|
3187
|
-
|
|
3188
|
-
|
|
3238
|
+
await applyBootstrapRecipe({
|
|
3239
|
+
session: sandboxSession.restricted(),
|
|
3240
|
+
recipe: sandboxBootstrapPlan.recipe,
|
|
3241
|
+
identity: sandboxBootstrapPlan.recipeIdentity,
|
|
3242
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
3243
|
+
abortSignal
|
|
3244
|
+
});
|
|
3189
3245
|
}
|
|
3190
3246
|
await ensureSandboxDirectory({
|
|
3191
3247
|
session: sandboxSession,
|
|
@@ -3573,14 +3629,13 @@ async function prepareHarnessSandboxTemplate(options) {
|
|
|
3573
3629
|
});
|
|
3574
3630
|
try {
|
|
3575
3631
|
if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
|
|
3576
|
-
await applyBootstrapRecipe(
|
|
3577
|
-
sandboxSession.restricted(),
|
|
3578
|
-
bootstrapPlan.recipe,
|
|
3579
|
-
bootstrapPlan.recipeIdentity,
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
);
|
|
3632
|
+
await applyBootstrapRecipe({
|
|
3633
|
+
session: sandboxSession.restricted(),
|
|
3634
|
+
recipe: bootstrapPlan.recipe,
|
|
3635
|
+
identity: bootstrapPlan.recipeIdentity,
|
|
3636
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
3637
|
+
abortSignal: options.abortSignal
|
|
3638
|
+
});
|
|
3584
3639
|
}
|
|
3585
3640
|
} finally {
|
|
3586
3641
|
await Promise.resolve(sandboxSession.stop()).catch(() => {
|
|
@@ -3607,6 +3662,7 @@ async function prepareSandboxForHarness(options) {
|
|
|
3607
3662
|
const workDir = sandboxConfig.workDir == null ? void 0 : normalizeSandboxWorkDir(sandboxConfig.workDir);
|
|
3608
3663
|
const recipeIdentities = {};
|
|
3609
3664
|
const skippedHarnessIds = [];
|
|
3665
|
+
let defaultWorkingDirectory;
|
|
3610
3666
|
for (const harness of harnesses) {
|
|
3611
3667
|
const recipe = await ((_b3 = harness.getBootstrap) == null ? void 0 : _b3.call(harness, {
|
|
3612
3668
|
abortSignal: options.abortSignal
|
|
@@ -3617,7 +3673,15 @@ async function prepareSandboxForHarness(options) {
|
|
|
3617
3673
|
}
|
|
3618
3674
|
const recipeIdentity = await hashHarnessBootstrap(recipe);
|
|
3619
3675
|
recipeIdentities[harness.harnessId] = recipeIdentity;
|
|
3620
|
-
|
|
3676
|
+
defaultWorkingDirectory != null ? defaultWorkingDirectory : defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
|
|
3677
|
+
session: options.session,
|
|
3678
|
+
abortSignal: options.abortSignal
|
|
3679
|
+
});
|
|
3680
|
+
await applyBootstrapRecipe({
|
|
3681
|
+
session: options.session,
|
|
3682
|
+
recipe,
|
|
3683
|
+
identity: recipeIdentity,
|
|
3684
|
+
defaultWorkingDirectory,
|
|
3621
3685
|
abortSignal: options.abortSignal
|
|
3622
3686
|
});
|
|
3623
3687
|
}
|
|
@@ -3626,6 +3690,7 @@ async function prepareSandboxForHarness(options) {
|
|
|
3626
3690
|
session: options.session,
|
|
3627
3691
|
workDir,
|
|
3628
3692
|
onBootstrap: sandboxConfig.onBootstrap,
|
|
3693
|
+
defaultWorkingDirectory,
|
|
3629
3694
|
abortSignal: options.abortSignal
|
|
3630
3695
|
});
|
|
3631
3696
|
}
|