@ai-sdk/harness 1.0.124 → 1.0.127

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness",
3
- "version": "1.0.124",
3
+ "version": "1.0.127",
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.47",
49
- "ai": "7.0.114"
48
+ "@ai-sdk/provider-utils": "5.0.49",
49
+ "ai": "7.0.117"
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.114",
61
+ "@ai-sdk/otel": "1.0.117",
62
62
  "@opentelemetry/sdk-trace-base": "2.7.1",
63
63
  "@types/node": "22.19.19",
64
64
  "@types/ws": "^8.5.13",
@@ -0,0 +1,80 @@
1
+ import type {
2
+ HarnessAgentAdapter,
3
+ HarnessSandboxTemplate,
4
+ } from './harness-agent-types';
5
+ import type { HarnessAgentSandboxConfig } from './harness-agent-settings';
6
+ import { hashHarnessBootstrap } from './internal/bootstrap-recipe';
7
+ import { resolvePreparedSandboxIdentity } from './internal/prepared-sandbox-identity';
8
+ import {
9
+ normalizeSandboxWorkDir,
10
+ runSandboxBootstrap,
11
+ validateSandboxBootstrapSettings,
12
+ } from './internal/sandbox-bootstrap';
13
+
14
+ export async function createHarnessSandboxTemplate(options: {
15
+ readonly harnesses: ReadonlyArray<HarnessAgentAdapter>;
16
+ readonly sandboxConfig?: Omit<HarnessAgentSandboxConfig, 'onSession'>;
17
+ }): Promise<HarnessSandboxTemplate | undefined> {
18
+ if (options.harnesses.length === 0) {
19
+ throw new Error(
20
+ 'createHarnessSandboxTemplate: at least one harness must be provided.',
21
+ );
22
+ }
23
+ const settings = options.sandboxConfig ?? {};
24
+ validateSandboxBootstrapSettings(settings);
25
+ const workDir =
26
+ settings.workDir == null
27
+ ? undefined
28
+ : normalizeSandboxWorkDir(settings.workDir);
29
+ const harnesses = [
30
+ ...new Map(
31
+ options.harnesses.map(harness => [harness.harnessId, harness]),
32
+ ).values(),
33
+ ].sort((a, b) => a.harnessId.localeCompare(b.harnessId));
34
+ const recipes = await Promise.all(
35
+ harnesses.map(async harness => {
36
+ const recipe = await harness.getBootstrap?.({});
37
+ return recipe == null
38
+ ? undefined
39
+ : {
40
+ recipe,
41
+ identity: await hashHarnessBootstrap(recipe),
42
+ };
43
+ }),
44
+ );
45
+ const recipeIdentities: Record<string, string> = {};
46
+ for (let index = 0; index < harnesses.length; index++) {
47
+ const entry = recipes[index];
48
+ if (entry != null)
49
+ recipeIdentities[harnesses[index].harnessId] = entry.identity;
50
+ }
51
+ const identity = await resolvePreparedSandboxIdentity({
52
+ recipeIdentities,
53
+ bootstrapHash: settings.bootstrapHash,
54
+ workDir,
55
+ });
56
+ if (identity == null) return undefined;
57
+ return {
58
+ identity,
59
+ prepare: async ({ session, abortSignal }) => {
60
+ for (const entry of recipes) {
61
+ if (entry != null) {
62
+ await runSandboxBootstrap({
63
+ session,
64
+ recipe: entry.recipe,
65
+ recipeIdentity: entry.identity,
66
+ abortSignal,
67
+ });
68
+ }
69
+ }
70
+ await runSandboxBootstrap({
71
+ session,
72
+ workDir,
73
+ onBootstrap: settings.onBootstrap,
74
+ bootstrapHash: settings.bootstrapHash,
75
+ skipOnBootstrapIfMarked: true,
76
+ abortSignal,
77
+ });
78
+ },
79
+ };
80
+ }
@@ -356,6 +356,7 @@ export type HarnessAgentSettings<
356
356
  * sessions. When omitted, every `createSession()` call must provide an
357
357
  * existing network sandbox session.
358
358
  */
359
+ /** @deprecated Supply `sandboxSession` to `HarnessAgent.createSession()` instead. */
359
360
  readonly sandbox?: HarnessV1SandboxProvider;
360
361
 
361
362
  /**
@@ -15,6 +15,7 @@ import type {
15
15
  HarnessV1PromptControl,
16
16
  HarnessV1PromptTurnOptions,
17
17
  HarnessV1ResumeSessionState,
18
+ HarnessV1SandboxTemplate,
18
19
  HarnessV1Session,
19
20
  HarnessV1Skill,
20
21
  HarnessV1StartOptions,
@@ -24,6 +25,7 @@ import type {
24
25
 
25
26
  export type HarnessAgentAdapter<TBuiltinTools extends ToolSet = ToolSet> =
26
27
  HarnessV1<TBuiltinTools>;
28
+ export type HarnessSandboxTemplate = HarnessV1SandboxTemplate;
27
29
 
28
30
  export type HarnessAgentBuiltinTool<
29
31
  INPUT = unknown,
@@ -48,6 +48,7 @@ import type {
48
48
  HarnessAgentResumeSessionState,
49
49
  HarnessAgentSkill,
50
50
  HarnessAgentToolSpec,
51
+ HarnessSandboxTemplate,
51
52
  } from './harness-agent-types';
52
53
  import { collectHarnessAgentToolApprovalContinuations } from './harness-agent-tool-approval-continuation';
53
54
  import { collectHarnessAgentToolResultContinuations } from './harness-agent-tool-result-continuation';
@@ -59,6 +60,7 @@ import {
59
60
  createSandboxBootstrapPlan,
60
61
  ensureSandboxDirectory,
61
62
  resolveSessionWorkDir,
63
+ runSandboxBootstrap,
62
64
  validateSandboxBootstrapSettings,
63
65
  } from './internal/sandbox-bootstrap';
64
66
  import { buildObservability } from './internal/resolve-observability';
@@ -72,6 +74,7 @@ import { resolveSandboxDefaultWorkingDirectory } from '../utils/resolve-sandbox-
72
74
  import { resolveSandboxHomeDir } from '../utils/sandbox-home-dir';
73
75
  import { getRestrictedSandboxSession } from '../utils/get-restricted-sandbox-session';
74
76
  import type { HarnessAgentLifecycleCallbacks } from './internal/turn-telemetry';
77
+ import { createHarnessSandboxTemplate } from './create-harness-sandbox-template';
75
78
 
76
79
  export type { HarnessAllTools } from './harness-agent-tool-types';
77
80
 
@@ -222,6 +225,11 @@ export class HarnessAgent<
222
225
  CALL_OPTIONS
223
226
  >,
224
227
  ) {
228
+ if (settings.sandbox != null) {
229
+ console.warn(
230
+ 'HarnessAgent: `sandbox` is deprecated. Supply `sandboxSession` to createSession() instead.',
231
+ );
232
+ }
225
233
  const sandboxConfig = resolveSandboxConfig(settings);
226
234
  validateSandboxBootstrapSettings(sandboxConfig);
227
235
  this.settings = settings;
@@ -290,6 +298,13 @@ export class HarnessAgent<
290
298
  return this.settings.output != null;
291
299
  }
292
300
 
301
+ getSandboxTemplate(): Promise<HarnessSandboxTemplate | undefined> {
302
+ return createHarnessSandboxTemplate({
303
+ harnesses: [this.settings.harness],
304
+ sandboxConfig: this.sandboxConfig,
305
+ });
306
+ }
307
+
293
308
  /**
294
309
  * Start a fresh session, or resume from state previously returned by
295
310
  * `session.detach()` or `session.stop()`. The returned
@@ -540,6 +555,14 @@ export class HarnessAgent<
540
555
  }
541
556
 
542
557
  try {
558
+ await runSandboxBootstrap({
559
+ session: getRestrictedSandboxSession(sandboxSession),
560
+ workDir: this.sandboxConfig.workDir,
561
+ onBootstrap: this.sandboxConfig.onBootstrap,
562
+ bootstrapHash: this.sandboxConfig.bootstrapHash,
563
+ skipOnBootstrapIfMarked: true,
564
+ abortSignal,
565
+ });
543
566
  await ensureSandboxDirectory({
544
567
  session: sandboxSession,
545
568
  workDir: sessionWorkDir,
@@ -0,0 +1,70 @@
1
+ import { posix } from 'node:path';
2
+ import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
3
+ import { harnessStateDirectoryPath } from '../../v1';
4
+ import { resolveSandboxHomeDir } from '../../utils/sandbox-home-dir';
5
+
6
+ export async function onBootstrapMarkerPath({
7
+ session,
8
+ bootstrapHash,
9
+ abortSignal,
10
+ }: {
11
+ session: SandboxSession;
12
+ bootstrapHash: string;
13
+ abortSignal?: AbortSignal;
14
+ }): Promise<string> {
15
+ const digest = new Uint8Array(
16
+ await crypto.subtle.digest(
17
+ 'SHA-256',
18
+ new TextEncoder().encode(bootstrapHash),
19
+ ),
20
+ );
21
+ const filename = Array.from(digest, byte =>
22
+ byte.toString(16).padStart(2, '0'),
23
+ ).join('');
24
+ return posix.join(
25
+ harnessStateDirectoryPath({
26
+ sandboxHomeDir: await resolveSandboxHomeDir({
27
+ sandbox: session,
28
+ abortSignal,
29
+ }),
30
+ }),
31
+ '.on-bootstrap',
32
+ `${filename}.ok`,
33
+ );
34
+ }
35
+
36
+ export async function hasOnBootstrapMarker(options: {
37
+ session: SandboxSession;
38
+ bootstrapHash: string;
39
+ abortSignal?: AbortSignal;
40
+ }): Promise<boolean> {
41
+ return (
42
+ (await options.session.readTextFile({
43
+ path: await onBootstrapMarkerPath(options),
44
+ abortSignal: options.abortSignal,
45
+ })) !== null
46
+ );
47
+ }
48
+
49
+ export async function writeOnBootstrapMarker(options: {
50
+ session: SandboxSession;
51
+ bootstrapHash: string;
52
+ abortSignal?: AbortSignal;
53
+ }): Promise<void> {
54
+ const path = await onBootstrapMarkerPath(options);
55
+ const result = await options.session.run({
56
+ command: 'mkdir -p "$MARKER_DIR"',
57
+ env: { MARKER_DIR: posix.dirname(path) },
58
+ abortSignal: options.abortSignal,
59
+ });
60
+ if (result.exitCode !== 0) {
61
+ throw new Error(
62
+ `Failed to create onBootstrap marker directory: ${result.stderr || result.stdout}`,
63
+ );
64
+ }
65
+ await options.session.writeTextFile({
66
+ path,
67
+ content: '',
68
+ abortSignal: options.abortSignal,
69
+ });
70
+ }
@@ -0,0 +1,42 @@
1
+ const PREPARED_SANDBOX_IDENTITY_VERSION = 1;
2
+
3
+ export async function resolvePreparedSandboxIdentity({
4
+ recipeIdentities,
5
+ bootstrapHash,
6
+ workDir,
7
+ }: {
8
+ readonly recipeIdentities: Record<string, string>;
9
+ readonly bootstrapHash?: string;
10
+ readonly workDir?: string;
11
+ }): Promise<string | undefined> {
12
+ const entries = Object.entries(recipeIdentities).sort(([a], [b]) =>
13
+ a.localeCompare(b),
14
+ );
15
+ if (entries.length === 0 && bootstrapHash == null) return undefined;
16
+
17
+ const encoder = new TextEncoder();
18
+ const chunks: Uint8Array[] = [];
19
+ const pushString = (value: string) => {
20
+ chunks.push(encoder.encode(value));
21
+ chunks.push(encoder.encode('\0'));
22
+ };
23
+ pushString(String(PREPARED_SANDBOX_IDENTITY_VERSION));
24
+ pushString(workDir ?? '');
25
+ pushString(bootstrapHash ?? '');
26
+ for (const [harnessId, identity] of entries) {
27
+ pushString(harnessId);
28
+ pushString(identity);
29
+ }
30
+ const buffer = new Uint8Array(
31
+ chunks.reduce((sum, chunk) => sum + chunk.length, 0),
32
+ );
33
+ let offset = 0;
34
+ for (const chunk of chunks) {
35
+ buffer.set(chunk, offset);
36
+ offset += chunk.length;
37
+ }
38
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', buffer));
39
+ return Array.from(digest.slice(0, 8), byte =>
40
+ byte.toString(16).padStart(2, '0'),
41
+ ).join('');
42
+ }
@@ -6,6 +6,10 @@ import { resolveSandboxDefaultWorkingDirectory } from '../../utils/resolve-sandb
6
6
  import { resolveSandboxHomeDir } from '../../utils/sandbox-home-dir';
7
7
  import type { HarnessAgentSandboxConfig } from '../harness-agent-settings';
8
8
  import { applyBootstrapRecipe, hashHarnessBootstrap } from './bootstrap-recipe';
9
+ import {
10
+ hasOnBootstrapMarker,
11
+ writeOnBootstrapMarker,
12
+ } from './on-bootstrap-marker';
9
13
 
10
14
  const SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
11
15
 
@@ -125,6 +129,7 @@ export async function createSandboxBootstrapPlan({
125
129
  recipeIdentity,
126
130
  workDir,
127
131
  onBootstrap: settings.onBootstrap,
132
+ bootstrapHash: settings.bootstrapHash,
128
133
  abortSignal: opts.abortSignal,
129
134
  }),
130
135
  }
@@ -138,6 +143,8 @@ export async function runSandboxBootstrap({
138
143
  recipeIdentity,
139
144
  workDir,
140
145
  onBootstrap,
146
+ bootstrapHash,
147
+ skipOnBootstrapIfMarked = false,
141
148
  defaultWorkingDirectory,
142
149
  abortSignal,
143
150
  }: {
@@ -146,6 +153,8 @@ export async function runSandboxBootstrap({
146
153
  readonly recipeIdentity?: string;
147
154
  readonly workDir?: string;
148
155
  readonly onBootstrap?: SandboxBootstrapSettings['onBootstrap'];
156
+ readonly bootstrapHash?: string;
157
+ readonly skipOnBootstrapIfMarked?: boolean;
149
158
  readonly defaultWorkingDirectory?: string;
150
159
  readonly abortSignal?: AbortSignal;
151
160
  }): Promise<void> {
@@ -173,6 +182,13 @@ export async function runSandboxBootstrap({
173
182
 
174
183
  if (onBootstrap == null) return;
175
184
 
185
+ if (
186
+ skipOnBootstrapIfMarked &&
187
+ bootstrapHash != null &&
188
+ (await hasOnBootstrapMarker({ session, bootstrapHash, abortSignal }))
189
+ )
190
+ return;
191
+
176
192
  const resolvedDefaultWorkingDirectory =
177
193
  defaultWorkingDirectory ??
178
194
  (await resolveSandboxDefaultWorkingDirectory({
@@ -194,6 +210,9 @@ export async function runSandboxBootstrap({
194
210
  abortSignal,
195
211
  });
196
212
  await onBootstrap({ session, workDir: bootstrapWorkDir, abortSignal });
213
+ if (bootstrapHash != null) {
214
+ await writeOnBootstrapMarker({ session, bootstrapHash, abortSignal });
215
+ }
197
216
  }
198
217
 
199
218
  export async function ensureSandboxDirectory({
@@ -25,8 +25,8 @@ type SandboxBootstrapSettings = Omit<HarnessAgentSandboxConfig, 'onSession'>;
25
25
  *
26
26
  * The temporary network sandbox session created during preparation is stopped
27
27
  * before the function resolves; the snapshot/template state persists in the
28
- * provider's native storage (for Vercel: as the `currentSnapshotId` of the
29
- * named template sandbox).
28
+ * provider's native storage.
29
+ * @deprecated Use `createHarnessSandboxTemplate` and pass its template to a sandbox session creator instead.
30
30
  */
31
31
  export async function prepareHarnessSandboxTemplate(options: {
32
32
  readonly harness: HarnessAgentAdapter;
@@ -34,6 +34,9 @@ export async function prepareHarnessSandboxTemplate(options: {
34
34
  readonly sandboxConfig?: SandboxBootstrapSettings;
35
35
  readonly abortSignal?: AbortSignal;
36
36
  }): Promise<void> {
37
+ console.warn(
38
+ 'prepareHarnessSandboxTemplate (prewarmHarness) is deprecated. Use createHarnessSandboxTemplate and a sandbox session creator instead.',
39
+ );
37
40
  const sandboxConfig = options.sandboxConfig ?? {};
38
41
  validateSandboxBootstrapSettings(sandboxConfig);
39
42
 
@@ -13,9 +13,9 @@ import {
13
13
  runSandboxBootstrap,
14
14
  validateSandboxBootstrapSettings,
15
15
  } from './internal/sandbox-bootstrap';
16
+ import { resolvePreparedSandboxIdentity } from './internal/prepared-sandbox-identity';
16
17
 
17
- const PREPARED_SANDBOX_IDENTITY_VERSION = 1;
18
-
18
+ /** @deprecated Use `createHarnessSandboxTemplate` and `template.prepare` instead. */
19
19
  export type PrepareSandboxForHarnessResult = {
20
20
  readonly identity?: string;
21
21
  readonly recipeIdentities: Record<string, string>;
@@ -41,6 +41,7 @@ export type PrepareSandboxForHarnessResult = {
41
41
  *
42
42
  * Repeated harness IDs are prepared once. When multiple adapters use the same
43
43
  * ID, the last adapter in `harnesses` is used.
44
+ * @deprecated Use `createHarnessSandboxTemplate` and `template.prepare` instead.
44
45
  */
45
46
  export async function prepareSandboxForHarness(options: {
46
47
  readonly session: SandboxSession;
@@ -48,6 +49,9 @@ export async function prepareSandboxForHarness(options: {
48
49
  readonly sandboxConfig?: HarnessAgentSandboxConfig;
49
50
  readonly abortSignal?: AbortSignal;
50
51
  }): Promise<PrepareSandboxForHarnessResult> {
52
+ console.warn(
53
+ 'prepareSandboxForHarness is deprecated. Use createHarnessSandboxTemplate and template.prepare instead.',
54
+ );
51
55
  const sandboxConfig = options.sandboxConfig ?? {};
52
56
  validateSandboxBootstrapSettings(sandboxConfig);
53
57
 
@@ -107,6 +111,7 @@ export async function prepareSandboxForHarness(options: {
107
111
  session: options.session,
108
112
  workDir,
109
113
  onBootstrap: sandboxConfig.onBootstrap,
114
+ bootstrapHash: sandboxConfig.bootstrapHash,
110
115
  defaultWorkingDirectory,
111
116
  abortSignal: options.abortSignal,
112
117
  });
@@ -124,52 +129,3 @@ export async function prepareSandboxForHarness(options: {
124
129
  skippedHarnessIds,
125
130
  };
126
131
  }
127
-
128
- async function resolvePreparedSandboxIdentity({
129
- recipeIdentities,
130
- bootstrapHash,
131
- workDir,
132
- }: {
133
- readonly recipeIdentities: Record<string, string>;
134
- readonly bootstrapHash?: string;
135
- readonly workDir?: string;
136
- }): Promise<string | undefined> {
137
- const entries = Object.entries(recipeIdentities).sort(([a], [b]) =>
138
- a.localeCompare(b),
139
- );
140
- if (entries.length === 0 && bootstrapHash == null) {
141
- return undefined;
142
- }
143
-
144
- const encoder = new TextEncoder();
145
- const chunks: Uint8Array[] = [];
146
- const pushString = (value: string) => {
147
- chunks.push(encoder.encode(value));
148
- chunks.push(encoder.encode('\0'));
149
- };
150
-
151
- pushString(String(PREPARED_SANDBOX_IDENTITY_VERSION));
152
- pushString(workDir ?? '');
153
- pushString(bootstrapHash ?? '');
154
-
155
- for (const [harnessId, identity] of entries) {
156
- pushString(harnessId);
157
- pushString(identity);
158
- }
159
-
160
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
161
- const buffer = new Uint8Array(totalLength);
162
- let offset = 0;
163
- for (const chunk of chunks) {
164
- buffer.set(chunk, offset);
165
- offset += chunk.length;
166
- }
167
-
168
- const digest = await crypto.subtle.digest('SHA-256', buffer);
169
- const bytes = new Uint8Array(digest);
170
- let hex = '';
171
- for (let i = 0; i < 8; i++) {
172
- hex += bytes[i].toString(16).padStart(2, '0');
173
- }
174
- return hex;
175
- }
@@ -34,8 +34,8 @@ export function harnessStateDirectoryPath({
34
34
  }
35
35
 
36
36
  /**
37
- * Network sandbox session returned by `HarnessV1SandboxProvider.createSession()`. The
38
- * harness keeps this for the lifetime of a session. It is itself a
37
+ * Network sandbox session returned by sandbox adapter creation and resume
38
+ * functions. The harness keeps this for the lifetime of a session. It is itself a
39
39
  * {@link SandboxSession} (file I/O, exec, spawn) and adds the infra surface on
40
40
  * top: port resolution, lifecycle, and network-policy mutation.
41
41
  *
@@ -46,22 +46,15 @@ export function harnessStateDirectoryPath({
46
46
  */
47
47
  export interface HarnessV1NetworkSandboxSession extends SandboxSession {
48
48
  /**
49
- * Stable identifier for the underlying sandbox resource. Used by the
50
- * harness session manager as the durable lookup key for cross-process
51
- * resume — the framework persists this on lifecycle state so a future
52
- * process can call `HarnessV1SandboxProvider.resume?({ sessionId })` and
53
- * reach the same resource. Providers populate it from their native
54
- * identifier (Vercel: the sandbox name; just-bash: a UUID minted at
55
- * create time).
49
+ * Identifier for the sandbox session. Persist this separately from the
50
+ * harness session ID and resume state when reattaching across processes.
56
51
  */
57
52
  readonly id: string;
58
53
 
59
54
  /**
60
55
  * The sandbox's default working directory — the absolute path that
61
56
  * `run`/`spawn` resolve relative commands against when no `workingDirectory`
62
- * is given. Read from the live sandbox (it is provider-specific and
63
- * configurable at create time: Vercel defaults to `/vercel/sandbox`,
64
- * just-bash to `/home/user`), never hardcoded.
57
+ * is given. Read from the live sandbox, never hardcoded.
65
58
  *
66
59
  * The framework composes each session's working directory underneath this
67
60
  * path (`<defaultWorkingDirectory>/<harnessId>-<sessionId>`) so adapters do
@@ -105,7 +98,7 @@ export interface HarnessV1NetworkSandboxSession extends SandboxSession {
105
98
 
106
99
  /**
107
100
  * Update the sandbox's outbound network policy. Optional — implementations
108
- * without a local enforcement primitive (e.g. just-bash) omit this. Callers
101
+ * without a local enforcement primitive omit this. Callers
109
102
  * use optional-call (`sandboxSession.setNetworkPolicy?.(policy)`); a
110
103
  * missing implementation is a no-op.
111
104
  */
@@ -139,7 +132,7 @@ export interface HarnessV1NetworkSandboxSession extends SandboxSession {
139
132
  /**
140
133
  * Replace the set of ports exposed by the sandbox. Full-replacement
141
134
  * semantics: ports omitted from the array are deregistered. Optional —
142
- * implementations that cannot expose ports (e.g. just-bash) omit this.
135
+ * implementations that cannot expose ports omit this.
143
136
  */
144
137
  readonly setPorts?: (
145
138
  ports: ReadonlyArray<number>,
@@ -7,6 +7,7 @@ import type { HarnessV1NetworkSandboxSession } from './harness-v1-network-sandbo
7
7
  * `LanguageModelV4` providers, no I/O performed at construction. The actual
8
8
  * sandbox is created (or wrapped) when `HarnessAgent` calls `createSession()`.
9
9
  */
10
+ /** @deprecated Supply a sandbox session to `HarnessAgent.createSession()` instead. */
10
11
  export interface HarnessV1SandboxProvider {
11
12
  readonly specificationVersion: 'harness-sandbox-v1';
12
13
  readonly providerId: string;
@@ -54,8 +55,8 @@ export interface HarnessV1SandboxProvider {
54
55
 
55
56
  /**
56
57
  * Reattach to an existing sandbox previously created with the same
57
- * `sessionId`. Optional — providers that cannot rehydrate by id (e.g.
58
- * just-bash) omit this; the harness throws
58
+ * `sessionId`. Optional — providers that cannot rehydrate by id omit
59
+ * this; the harness throws
59
60
  * `HarnessCapabilityUnsupportedError` when resume is attempted against
60
61
  * them.
61
62
  *
@@ -0,0 +1,14 @@
1
+ import type { HarnessV1SandboxTemplate } from './harness-v1-sandbox-template';
2
+
3
+ export type HarnessV1SandboxSessionCreateOptions<
4
+ TProviderOptions extends object,
5
+ > = TProviderOptions & {
6
+ /**
7
+ * Manually assign the ID of a newly created sandbox session. Creation never
8
+ * looks up or resumes an existing session. An existing sandbox resource with
9
+ * the same ID is a creation conflict.
10
+ */
11
+ readonly sandboxId?: string;
12
+ readonly template?: HarnessV1SandboxTemplate;
13
+ readonly abortSignal?: AbortSignal;
14
+ };
@@ -0,0 +1,10 @@
1
+ export type HarnessV1SandboxSessionResumeOptions<
2
+ TProviderOptions extends object,
3
+ > = TProviderOptions & {
4
+ /**
5
+ * ID of the existing sandbox session to reattach. Resume never creates a
6
+ * sandbox when the ID cannot be found.
7
+ */
8
+ readonly sandboxId: string;
9
+ readonly abortSignal?: AbortSignal;
10
+ };
@@ -0,0 +1,9 @@
1
+ import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
2
+
3
+ export type HarnessV1SandboxTemplate = {
4
+ readonly identity: string;
5
+ readonly prepare: (options: {
6
+ readonly session: Experimental_SandboxSession;
7
+ readonly abortSignal?: AbortSignal;
8
+ }) => Promise<void>;
9
+ };
package/src/v1/index.ts CHANGED
@@ -48,6 +48,9 @@ export type {
48
48
  HarnessV1ResponseFormat,
49
49
  } from './harness-v1-response-format';
50
50
  export type { HarnessV1SandboxProvider } from './harness-v1-sandbox-provider';
51
+ export type { HarnessV1SandboxTemplate } from './harness-v1-sandbox-template';
52
+ export type { HarnessV1SandboxSessionCreateOptions } from './harness-v1-sandbox-session-create-options';
53
+ export type { HarnessV1SandboxSessionResumeOptions } from './harness-v1-sandbox-session-resume-options';
51
54
  export type {
52
55
  HarnessV1ContinueTurnState,
53
56
  HarnessV1LifecycleState,