@ai-sdk/harness 1.0.1 → 1.0.3
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 +12 -0
- package/README.md +12 -0
- package/agent/index.ts +9 -1
- package/dist/agent/index.d.ts +19 -5
- package/dist/agent/index.js +106 -1
- package/dist/agent/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agent/prepare-sandbox-for-harness.ts +148 -0
- package/src/agent/prewarm.ts +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@ai-sdk/provider": "4.0.0",
|
|
48
48
|
"@ai-sdk/provider-utils": "5.0.0",
|
|
49
|
-
"ai": "7.0.
|
|
49
|
+
"ai": "7.0.2"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"ws": "^8.21.0"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
|
+
import type { HarnessAgentSandboxConfig } from './harness-agent-settings';
|
|
3
|
+
import type { HarnessAgentAdapter } from './harness-agent-types';
|
|
4
|
+
import {
|
|
5
|
+
applyBootstrapRecipe,
|
|
6
|
+
hashHarnessBootstrap,
|
|
7
|
+
} from './internal/bootstrap-recipe';
|
|
8
|
+
import {
|
|
9
|
+
normalizeSandboxWorkDir,
|
|
10
|
+
runSandboxBootstrap,
|
|
11
|
+
validateSandboxBootstrapSettings,
|
|
12
|
+
} from './internal/sandbox-bootstrap';
|
|
13
|
+
|
|
14
|
+
const PREPARED_SANDBOX_IDENTITY_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
export type PrepareSandboxForHarnessResult = {
|
|
17
|
+
readonly identity?: string;
|
|
18
|
+
readonly recipeIdentities: Record<string, string>;
|
|
19
|
+
readonly skippedHarnessIds: ReadonlyArray<string>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function prepareSandboxForHarness(options: {
|
|
23
|
+
readonly session: SandboxSession;
|
|
24
|
+
readonly harnesses: ReadonlyArray<HarnessAgentAdapter>;
|
|
25
|
+
readonly sandboxConfig?: HarnessAgentSandboxConfig;
|
|
26
|
+
readonly abortSignal?: AbortSignal;
|
|
27
|
+
}): Promise<PrepareSandboxForHarnessResult> {
|
|
28
|
+
const sandboxConfig = options.sandboxConfig ?? {};
|
|
29
|
+
validateSandboxBootstrapSettings(sandboxConfig);
|
|
30
|
+
|
|
31
|
+
if (options.harnesses.length === 0) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'prepareSandboxForHarness: at least one harness must be provided.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const harnesses = [...options.harnesses].sort((a, b) =>
|
|
38
|
+
a.harnessId.localeCompare(b.harnessId),
|
|
39
|
+
);
|
|
40
|
+
assertUniqueHarnessIds(harnesses);
|
|
41
|
+
|
|
42
|
+
const workDir =
|
|
43
|
+
sandboxConfig.workDir == null
|
|
44
|
+
? undefined
|
|
45
|
+
: normalizeSandboxWorkDir(sandboxConfig.workDir);
|
|
46
|
+
const recipeIdentities: Record<string, string> = {};
|
|
47
|
+
const skippedHarnessIds: string[] = [];
|
|
48
|
+
|
|
49
|
+
for (const harness of harnesses) {
|
|
50
|
+
const recipe = await harness.getBootstrap?.({
|
|
51
|
+
abortSignal: options.abortSignal,
|
|
52
|
+
});
|
|
53
|
+
if (recipe == null) {
|
|
54
|
+
skippedHarnessIds.push(harness.harnessId);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const recipeIdentity = await hashHarnessBootstrap(recipe);
|
|
59
|
+
recipeIdentities[harness.harnessId] = recipeIdentity;
|
|
60
|
+
await applyBootstrapRecipe(options.session, recipe, recipeIdentity, {
|
|
61
|
+
abortSignal: options.abortSignal,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (sandboxConfig.onBootstrap != null) {
|
|
66
|
+
await runSandboxBootstrap({
|
|
67
|
+
session: options.session,
|
|
68
|
+
workDir,
|
|
69
|
+
onBootstrap: sandboxConfig.onBootstrap,
|
|
70
|
+
abortSignal: options.abortSignal,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const identity = await resolvePreparedSandboxIdentity({
|
|
75
|
+
recipeIdentities,
|
|
76
|
+
bootstrapHash: sandboxConfig.bootstrapHash,
|
|
77
|
+
workDir,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...(identity != null ? { identity } : {}),
|
|
82
|
+
recipeIdentities,
|
|
83
|
+
skippedHarnessIds,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertUniqueHarnessIds(
|
|
88
|
+
harnesses: ReadonlyArray<HarnessAgentAdapter>,
|
|
89
|
+
): void {
|
|
90
|
+
const seen = new Set<string>();
|
|
91
|
+
for (const harness of harnesses) {
|
|
92
|
+
if (seen.has(harness.harnessId)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`prepareSandboxForHarness: duplicate harness id "${harness.harnessId}".`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
seen.add(harness.harnessId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolvePreparedSandboxIdentity({
|
|
102
|
+
recipeIdentities,
|
|
103
|
+
bootstrapHash,
|
|
104
|
+
workDir,
|
|
105
|
+
}: {
|
|
106
|
+
readonly recipeIdentities: Record<string, string>;
|
|
107
|
+
readonly bootstrapHash?: string;
|
|
108
|
+
readonly workDir?: string;
|
|
109
|
+
}): Promise<string | undefined> {
|
|
110
|
+
const entries = Object.entries(recipeIdentities).sort(([a], [b]) =>
|
|
111
|
+
a.localeCompare(b),
|
|
112
|
+
);
|
|
113
|
+
if (entries.length === 0 && bootstrapHash == null) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const encoder = new TextEncoder();
|
|
118
|
+
const chunks: Uint8Array[] = [];
|
|
119
|
+
const pushString = (value: string) => {
|
|
120
|
+
chunks.push(encoder.encode(value));
|
|
121
|
+
chunks.push(encoder.encode('\0'));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
pushString(String(PREPARED_SANDBOX_IDENTITY_VERSION));
|
|
125
|
+
pushString(workDir ?? '');
|
|
126
|
+
pushString(bootstrapHash ?? '');
|
|
127
|
+
|
|
128
|
+
for (const [harnessId, identity] of entries) {
|
|
129
|
+
pushString(harnessId);
|
|
130
|
+
pushString(identity);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
134
|
+
const buffer = new Uint8Array(totalLength);
|
|
135
|
+
let offset = 0;
|
|
136
|
+
for (const chunk of chunks) {
|
|
137
|
+
buffer.set(chunk, offset);
|
|
138
|
+
offset += chunk.length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const digest = await crypto.subtle.digest('SHA-256', buffer);
|
|
142
|
+
const bytes = new Uint8Array(digest);
|
|
143
|
+
let hex = '';
|
|
144
|
+
for (let i = 0; i < 8; i++) {
|
|
145
|
+
hex += bytes[i].toString(16).padStart(2, '0');
|
|
146
|
+
}
|
|
147
|
+
return hex;
|
|
148
|
+
}
|
package/src/agent/prewarm.ts
CHANGED
|
@@ -10,20 +10,20 @@ import {
|
|
|
10
10
|
type SandboxBootstrapSettings = Omit<HarnessAgentSandboxConfig, 'onSession'>;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Prepare a harness's sandbox template without running an agent. Idempotent: if
|
|
14
|
+
* the template already exists (snapshot present, or marker on a non-snapshot
|
|
15
15
|
* provider), this resolves quickly.
|
|
16
16
|
*
|
|
17
17
|
* Use from a CI/deploy script to amortize the first-session cost so production
|
|
18
18
|
* sessions always resume from snapshot. For adapters without a bootstrap
|
|
19
19
|
* recipe (no `getBootstrap`) this is a no-op.
|
|
20
20
|
*
|
|
21
|
-
* The temporary network sandbox session created during
|
|
21
|
+
* The temporary network sandbox session created during preparation is stopped
|
|
22
22
|
* before the function resolves; the snapshot/template state persists in the
|
|
23
23
|
* provider's native storage (for Vercel: as the `currentSnapshotId` of the
|
|
24
24
|
* named template sandbox).
|
|
25
25
|
*/
|
|
26
|
-
export async function
|
|
26
|
+
export async function prepareHarnessSandboxTemplate(options: {
|
|
27
27
|
readonly harness: HarnessAgentAdapter;
|
|
28
28
|
readonly sandboxProvider: HarnessV1SandboxProvider;
|
|
29
29
|
readonly sandboxConfig?: SandboxBootstrapSettings;
|
|
@@ -63,3 +63,6 @@ export async function prewarmHarness(options: {
|
|
|
63
63
|
await Promise.resolve(sandboxSession.stop()).catch(() => {});
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/** @deprecated Use `prepareHarnessSandboxTemplate` instead. */
|
|
68
|
+
export const prewarmHarness = prepareHarnessSandboxTemplate;
|