@ai-sdk/harness 1.0.74 → 1.0.76
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 +14 -0
- package/README.md +6 -6
- package/dist/agent/index.d.ts +93 -105
- package/dist/agent/index.js +153 -180
- package/dist/agent/index.js.map +1 -1
- package/dist/index.d.ts +0 -11
- package/package.json +3 -3
- package/src/agent/harness-agent-session.ts +18 -44
- package/src/agent/harness-agent-settings.ts +4 -5
- package/src/agent/harness-agent.ts +127 -149
- package/src/agent/internal/harness-stream-text-result.ts +1 -12
- package/src/agent/internal/run-prompt.ts +17 -1
- package/src/agent/internal/translate-stream-part.ts +39 -0
- package/src/agent/prepare-harness-sandbox-template.ts +15 -7
- package/src/v1/harness-v1-sandbox-provider.ts +0 -12
- package/src/agent/internal/bridge-port-registry.ts +0 -52
package/dist/index.d.ts
CHANGED
|
@@ -1263,17 +1263,6 @@ declare function commonTool<TName extends HarnessV1BuiltinToolName, TInput>(comm
|
|
|
1263
1263
|
interface HarnessV1SandboxProvider {
|
|
1264
1264
|
readonly specificationVersion: 'harness-sandbox-v1';
|
|
1265
1265
|
readonly providerId: string;
|
|
1266
|
-
/**
|
|
1267
|
-
* Pool of ports the consumer reserved on a caller-provided sandbox for
|
|
1268
|
-
* concurrent harness sessions. The session manager leases one port per
|
|
1269
|
-
* session and releases on stop or destroy.
|
|
1270
|
-
*
|
|
1271
|
-
* Only meaningful when the provider wraps a caller-provided sandbox
|
|
1272
|
-
* (the caller pre-declared the ports). In create-new modes the provider
|
|
1273
|
-
* mints a fresh sandbox per session, so no leasing is needed; providers
|
|
1274
|
-
* leave this undefined.
|
|
1275
|
-
*/
|
|
1276
|
-
readonly bridgePorts?: ReadonlyArray<number>;
|
|
1277
1266
|
/**
|
|
1278
1267
|
* Providers should throw `HarnessSandboxAuthenticationError` when sandbox
|
|
1279
1268
|
* acquisition fails because credentials are missing, invalid, or not
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.76",
|
|
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.7",
|
|
48
48
|
"@ai-sdk/provider-utils": "5.0.27",
|
|
49
|
-
"ai": "7.0.
|
|
49
|
+
"ai": "7.0.69"
|
|
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.69",
|
|
69
69
|
"@vercel/ai-tsconfig": "0.0.0"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
@@ -10,7 +10,6 @@ import type {
|
|
|
10
10
|
HarnessV1BuiltinToolFiltering,
|
|
11
11
|
HarnessV1NetworkSandboxSession,
|
|
12
12
|
HarnessV1ResponseFormat,
|
|
13
|
-
HarnessV1SandboxProvider,
|
|
14
13
|
} from '../v1';
|
|
15
14
|
import type {
|
|
16
15
|
HarnessAgentAdapter,
|
|
@@ -24,7 +23,6 @@ import type {
|
|
|
24
23
|
} from './harness-agent-types';
|
|
25
24
|
import type { HarnessAgentToolApprovalContinuation } from './harness-agent-tool-approval-continuation';
|
|
26
25
|
import type { HarnessAgentToolResultContinuation } from './harness-agent-tool-result-continuation';
|
|
27
|
-
import { releaseBridgePort } from './internal/bridge-port-registry';
|
|
28
26
|
import { validateLifecycleStateData } from './internal/lifecycle-state-validation';
|
|
29
27
|
import { runPrompt } from './internal/run-prompt';
|
|
30
28
|
|
|
@@ -50,9 +48,7 @@ type HarnessAgentTurnState =
|
|
|
50
48
|
* Live harness session held by the caller.
|
|
51
49
|
*
|
|
52
50
|
* Created by {@link import('./harness-agent').HarnessAgent.createSession}.
|
|
53
|
-
* Owns the underlying adapter session
|
|
54
|
-
* bridge-port lease (when the provider wraps a caller-provided sandbox with a
|
|
55
|
-
* port pool).
|
|
51
|
+
* Owns the underlying adapter session and the network sandbox session.
|
|
56
52
|
*
|
|
57
53
|
* Pass the instance back to `agent.generate` / `agent.stream` on every
|
|
58
54
|
* call; end the local handle with `detach()`, `stop()`, or `destroy()`.
|
|
@@ -69,11 +65,10 @@ export class HarnessAgentSession {
|
|
|
69
65
|
readonly sessionId: string;
|
|
70
66
|
|
|
71
67
|
private readonly harness: HarnessAgentAdapter;
|
|
72
|
-
private readonly sandboxProvider: HarnessV1SandboxProvider;
|
|
73
68
|
private readonly sessionWorkDir: string;
|
|
69
|
+
private readonly ownsSandboxLifecycle: boolean;
|
|
74
70
|
private underlyingSession: HarnessAgentAdapterSession | undefined;
|
|
75
71
|
private sandboxSession: HarnessV1NetworkSandboxSession | undefined;
|
|
76
|
-
private leasedBridgePort: number | undefined;
|
|
77
72
|
private readonly toolApproval:
|
|
78
73
|
| HarnessAgentToolApprovalConfiguration
|
|
79
74
|
| undefined;
|
|
@@ -104,8 +99,7 @@ export class HarnessAgentSession {
|
|
|
104
99
|
harness: HarnessAgentAdapter;
|
|
105
100
|
underlyingSession: HarnessAgentAdapterSession;
|
|
106
101
|
sandboxSession: HarnessV1NetworkSandboxSession;
|
|
107
|
-
|
|
108
|
-
leasedBridgePort?: number;
|
|
102
|
+
ownsSandboxLifecycle?: boolean;
|
|
109
103
|
sessionWorkDir: string;
|
|
110
104
|
toolApproval: HarnessAgentToolApprovalConfiguration | undefined;
|
|
111
105
|
pendingToolApprovals?: readonly HarnessAgentPendingToolApproval[];
|
|
@@ -116,8 +110,7 @@ export class HarnessAgentSession {
|
|
|
116
110
|
this.harness = options.harness;
|
|
117
111
|
this.underlyingSession = options.underlyingSession;
|
|
118
112
|
this.sandboxSession = options.sandboxSession;
|
|
119
|
-
this.
|
|
120
|
-
this.leasedBridgePort = options.leasedBridgePort;
|
|
113
|
+
this.ownsSandboxLifecycle = options.ownsSandboxLifecycle ?? true;
|
|
121
114
|
this.sessionWorkDir = options.sessionWorkDir;
|
|
122
115
|
this.toolApproval = options.toolApproval;
|
|
123
116
|
for (const approval of options.pendingToolApprovals ?? []) {
|
|
@@ -362,15 +355,13 @@ export class HarnessAgentSession {
|
|
|
362
355
|
});
|
|
363
356
|
return validated;
|
|
364
357
|
} finally {
|
|
365
|
-
this.endLocalHandle({
|
|
366
|
-
sessionState: 'detached',
|
|
367
|
-
releasePortLease: false,
|
|
368
|
-
});
|
|
358
|
+
this.endLocalHandle({ sessionState: 'detached' });
|
|
369
359
|
}
|
|
370
360
|
}
|
|
371
361
|
|
|
372
362
|
/**
|
|
373
|
-
* Persist enough state to resume later, then stop the runtime and
|
|
363
|
+
* Persist enough state to resume later, then stop the runtime and any
|
|
364
|
+
* harness-owned sandbox.
|
|
374
365
|
* Returns the resume state for a future
|
|
375
366
|
* `agent.createSession({ sessionId, resumeFrom })` call.
|
|
376
367
|
*/
|
|
@@ -396,26 +387,26 @@ export class HarnessAgentSession {
|
|
|
396
387
|
});
|
|
397
388
|
return validated;
|
|
398
389
|
} finally {
|
|
399
|
-
this.endLocalHandle({
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
await Promise.resolve(sandboxSession.stop()).catch(() => {});
|
|
390
|
+
this.endLocalHandle({ sessionState: 'stopped' });
|
|
391
|
+
if (this.ownsSandboxLifecycle) {
|
|
392
|
+
await Promise.resolve(sandboxSession.stop()).catch(() => {});
|
|
393
|
+
}
|
|
404
394
|
}
|
|
405
395
|
}
|
|
406
396
|
|
|
407
397
|
/**
|
|
408
|
-
* Stop the runtime and discard resumability.
|
|
409
|
-
*
|
|
398
|
+
* Stop the runtime and discard resumability. A harness-owned sandbox is
|
|
399
|
+
* destroyed when supported; otherwise it is stopped.
|
|
410
400
|
*/
|
|
411
401
|
async destroy(): Promise<void> {
|
|
412
402
|
if (this.sessionState !== 'active') return;
|
|
413
403
|
const session = this.underlyingSession;
|
|
414
404
|
const sandboxSession = this.getSandboxSession();
|
|
415
|
-
this.endLocalHandle({ sessionState: 'destroyed'
|
|
405
|
+
this.endLocalHandle({ sessionState: 'destroyed' });
|
|
416
406
|
if (session != null) {
|
|
417
407
|
await Promise.resolve(session.doDestroy()).catch(() => {});
|
|
418
408
|
}
|
|
409
|
+
if (!this.ownsSandboxLifecycle) return;
|
|
419
410
|
await Promise.resolve(
|
|
420
411
|
sandboxSession.destroy?.() ?? sandboxSession.stop(),
|
|
421
412
|
).catch(() => {});
|
|
@@ -430,9 +421,8 @@ export class HarnessAgentSession {
|
|
|
430
421
|
*
|
|
431
422
|
* After this call the session is detached. This in-process handle no
|
|
432
423
|
* longer drives turns; a future slice creates a fresh session from the
|
|
433
|
-
* returned state. The sandbox is **not** stopped
|
|
434
|
-
*
|
|
435
|
-
* that port.
|
|
424
|
+
* returned state. The sandbox is **not** stopped because bridge-backed
|
|
425
|
+
* adapters may still have a live bridge.
|
|
436
426
|
*/
|
|
437
427
|
async suspendTurn(): Promise<HarnessAgentContinueTurnState> {
|
|
438
428
|
if (this.sessionState !== 'active' || this.underlyingSession == null) {
|
|
@@ -449,10 +439,7 @@ export class HarnessAgentSession {
|
|
|
449
439
|
try {
|
|
450
440
|
return await this.suspendCurrentTurn({ session });
|
|
451
441
|
} finally {
|
|
452
|
-
this.endLocalHandle({
|
|
453
|
-
sessionState: 'detached',
|
|
454
|
-
releasePortLease: false,
|
|
455
|
-
});
|
|
442
|
+
this.endLocalHandle({ sessionState: 'detached' });
|
|
456
443
|
}
|
|
457
444
|
}
|
|
458
445
|
|
|
@@ -588,23 +575,10 @@ export class HarnessAgentSession {
|
|
|
588
575
|
|
|
589
576
|
private endLocalHandle(options: {
|
|
590
577
|
sessionState: Exclude<HarnessAgentSessionState, 'active'>;
|
|
591
|
-
releasePortLease: boolean;
|
|
592
578
|
}): void {
|
|
593
579
|
this.sessionState = options.sessionState;
|
|
594
580
|
this.underlyingSession = undefined;
|
|
595
581
|
this.sandboxSession = undefined;
|
|
596
|
-
if (options.releasePortLease) {
|
|
597
|
-
this.releasePortLease();
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
private releasePortLease(): void {
|
|
602
|
-
if (this.leasedBridgePort == null) return;
|
|
603
|
-
releaseBridgePort({
|
|
604
|
-
poolKey: this.sandboxProvider,
|
|
605
|
-
sessionId: this.sessionId,
|
|
606
|
-
});
|
|
607
|
-
this.leasedBridgePort = undefined;
|
|
608
582
|
}
|
|
609
583
|
|
|
610
584
|
private requireReusableSession(): HarnessAgentAdapterSession {
|
|
@@ -179,12 +179,11 @@ export type HarnessAgentSettings<
|
|
|
179
179
|
readonly toolApproval?: HarnessAgentToolApprovalConfiguration;
|
|
180
180
|
|
|
181
181
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* `Experimental_SandboxSession` so tools cannot reach the infra surface.
|
|
182
|
+
* Optional sandbox provider used to create or resume network sandbox
|
|
183
|
+
* sessions. When omitted, every `createSession()` call must provide an
|
|
184
|
+
* existing network sandbox session.
|
|
186
185
|
*/
|
|
187
|
-
readonly sandbox
|
|
186
|
+
readonly sandbox?: HarnessV1SandboxProvider;
|
|
188
187
|
|
|
189
188
|
/**
|
|
190
189
|
* Sandbox working-directory and lifecycle hook configuration.
|
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
HarnessV1JSONSchema,
|
|
6
6
|
HarnessV1NetworkSandboxSession,
|
|
7
7
|
HarnessV1ResponseFormat,
|
|
8
|
-
HarnessV1SandboxProvider,
|
|
9
8
|
} from '../v1';
|
|
10
9
|
import {
|
|
11
10
|
asArray,
|
|
@@ -48,17 +47,15 @@ import {
|
|
|
48
47
|
collectHarnessAgentToolResultContinuations,
|
|
49
48
|
type HarnessAgentToolResultContinuation,
|
|
50
49
|
} from './harness-agent-tool-result-continuation';
|
|
51
|
-
import { applyBootstrapRecipe } from './internal/bootstrap-recipe';
|
|
52
50
|
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} from './internal/
|
|
51
|
+
applyBootstrapRecipe,
|
|
52
|
+
hashHarnessBootstrap,
|
|
53
|
+
} from './internal/bootstrap-recipe';
|
|
56
54
|
import {
|
|
57
55
|
createSandboxBootstrapPlan,
|
|
58
56
|
ensureSandboxDirectory,
|
|
59
57
|
resolveSessionWorkDir,
|
|
60
58
|
validateSandboxBootstrapSettings,
|
|
61
|
-
type SandboxBootstrapPlan,
|
|
62
59
|
} from './internal/sandbox-bootstrap';
|
|
63
60
|
import { buildObservability } from './internal/resolve-observability';
|
|
64
61
|
import { validateLifecycleStateData } from './internal/lifecycle-state-validation';
|
|
@@ -110,12 +107,13 @@ export interface HarnessAgentCallExtensions {
|
|
|
110
107
|
* the result is fed back to the harness via `submitToolResult`.
|
|
111
108
|
* Adapter builtin tools (e.g. Claude Code's `Bash`) pass through
|
|
112
109
|
* untouched.
|
|
113
|
-
* - **Sandbox propagation.** `
|
|
114
|
-
*
|
|
115
|
-
* `resumeSession()`)
|
|
116
|
-
* `doStart`. Its `restricted()` view (a tool-safe
|
|
110
|
+
* - **Sandbox propagation.** On `createSession`, the agent uses a
|
|
111
|
+
* caller-provided network sandbox session when present; otherwise it calls
|
|
112
|
+
* the configured provider's `createSession()` (or `resumeSession()`). It
|
|
113
|
+
* passes the selected session into `doStart`. Its `restricted()` view (a tool-safe
|
|
117
114
|
* `Experimental_SandboxSession`) is handed to user-tool `execute()` calls
|
|
118
|
-
* via `experimental_sandbox`.
|
|
115
|
+
* via `experimental_sandbox`. Caller-provided sandboxes remain owned by the
|
|
116
|
+
* caller and are not stopped or destroyed by the harness layer.
|
|
119
117
|
*/
|
|
120
118
|
export class HarnessAgent<
|
|
121
119
|
THarness extends HarnessAgentAdapter<any> = HarnessAgentAdapter,
|
|
@@ -235,14 +233,21 @@ export class HarnessAgent<
|
|
|
235
233
|
* handing it to the adapter.
|
|
236
234
|
*/
|
|
237
235
|
continueFrom?: HarnessAgentContinueTurnState;
|
|
236
|
+
/**
|
|
237
|
+
* Existing network sandbox session to run the harness in. When provided,
|
|
238
|
+
* the caller retains ownership of the sandbox lifecycle.
|
|
239
|
+
*/
|
|
240
|
+
sandboxSession?: HarnessV1NetworkSandboxSession;
|
|
238
241
|
abortSignal?: AbortSignal;
|
|
239
242
|
}): Promise<HarnessAgentSession> {
|
|
240
243
|
const sessionId = options?.sessionId ?? generateId();
|
|
241
244
|
const resumeFrom = options?.resumeFrom;
|
|
242
245
|
const continueFrom = options?.continueFrom;
|
|
246
|
+
const providedSandboxSession = options?.sandboxSession;
|
|
243
247
|
const abortSignal = options?.abortSignal;
|
|
244
248
|
const harness = this.settings.harness;
|
|
245
249
|
const sandboxProvider = this.settings.sandbox;
|
|
250
|
+
const ownsSandboxLifecycle = providedSandboxSession == null;
|
|
246
251
|
|
|
247
252
|
if (resumeFrom != null && continueFrom != null) {
|
|
248
253
|
throw new Error(
|
|
@@ -273,61 +278,121 @@ export class HarnessAgent<
|
|
|
273
278
|
const isResumedSession =
|
|
274
279
|
validatedResumeFrom != null || effectiveContinueFrom != null;
|
|
275
280
|
|
|
276
|
-
let recipe: HarnessV1Bootstrap | undefined;
|
|
277
|
-
if (harness.getBootstrap != null) {
|
|
278
|
-
recipe = await harness.getBootstrap({ abortSignal });
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Defines the hashes based on both harness bootstrap recipe and
|
|
282
|
-
// consumer-defined onBootstrap callback.
|
|
283
|
-
const sandboxBootstrapPlan = await createSandboxBootstrapPlan({
|
|
284
|
-
recipe,
|
|
285
|
-
settings: this.sandboxConfig,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
281
|
// Acquires the concrete sandbox session, either by starting fresh and then
|
|
289
282
|
// creating a post-bootstrap snapshot, or by reusing a previously created
|
|
290
283
|
// snapshot based on the bootstrap-based hashes.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
284
|
+
let sandboxSession: HarnessV1NetworkSandboxSession;
|
|
285
|
+
let sessionWorkDir: string;
|
|
286
|
+
if (providedSandboxSession != null) {
|
|
287
|
+
sandboxSession = providedSandboxSession;
|
|
288
|
+
sessionWorkDir = resolveSessionWorkDir({
|
|
289
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
290
|
+
harnessId: harness.harnessId,
|
|
291
|
+
sessionId,
|
|
292
|
+
workDir: this.sandboxConfig.workDir,
|
|
293
|
+
});
|
|
298
294
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
295
|
+
const recipe = await harness.getBootstrap?.({ abortSignal });
|
|
296
|
+
if (recipe != null) {
|
|
297
|
+
const recipeIdentity = await hashHarnessBootstrap(recipe);
|
|
298
|
+
try {
|
|
299
|
+
await applyBootstrapRecipe({
|
|
300
|
+
session: sandboxSession.restricted(),
|
|
301
|
+
recipe,
|
|
302
|
+
identity: recipeIdentity,
|
|
303
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
304
|
+
abortSignal,
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
await cleanupAfterStartFailure({
|
|
308
|
+
sandboxSession,
|
|
309
|
+
ownsSandboxLifecycle,
|
|
310
|
+
});
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
if (sandboxProvider == null) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
'HarnessAgent.createSession: configure `sandbox` on HarnessAgent or pass `sandboxSession` to createSession().',
|
|
318
|
+
);
|
|
319
|
+
}
|
|
312
320
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
recipe: sandboxBootstrapPlan.recipe,
|
|
326
|
-
identity: sandboxBootstrapPlan.recipeIdentity,
|
|
321
|
+
if (isResumedSession) {
|
|
322
|
+
if (sandboxProvider.resumeSession == null) {
|
|
323
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
324
|
+
message: `Sandbox provider '${sandboxProvider.providerId}' does not support resume.`,
|
|
325
|
+
harnessId: harness.harnessId,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
sandboxSession = await sandboxProvider.resumeSession({
|
|
329
|
+
sessionId,
|
|
330
|
+
abortSignal,
|
|
331
|
+
});
|
|
332
|
+
sessionWorkDir = resolveSessionWorkDir({
|
|
327
333
|
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
334
|
+
harnessId: harness.harnessId,
|
|
335
|
+
sessionId,
|
|
336
|
+
workDir: this.sandboxConfig.workDir,
|
|
337
|
+
});
|
|
338
|
+
} else {
|
|
339
|
+
// The logic in this clause applies the bootstrap plan, including both the harness
|
|
340
|
+
// bootstrap recipe and agent specific sandbox configuration.
|
|
341
|
+
// The logic matches largely what `prepareHarnessSandboxTemplate()` and
|
|
342
|
+
// `prepareSandboxForHarness()` do, so they will have to remain aligned.
|
|
343
|
+
let recipe: HarnessV1Bootstrap | undefined;
|
|
344
|
+
if (harness.getBootstrap != null) {
|
|
345
|
+
recipe = await harness.getBootstrap({ abortSignal });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Defines the hashes based on both harness bootstrap recipe and
|
|
349
|
+
// consumer-defined onBootstrap callback.
|
|
350
|
+
const sandboxBootstrapPlan = await createSandboxBootstrapPlan({
|
|
351
|
+
recipe,
|
|
352
|
+
settings: this.sandboxConfig,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
sandboxSession = await sandboxProvider.createSession({
|
|
356
|
+
sessionId,
|
|
328
357
|
abortSignal,
|
|
358
|
+
identity: sandboxBootstrapPlan.identity,
|
|
359
|
+
onFirstCreate: sandboxBootstrapPlan.onFirstCreate,
|
|
329
360
|
});
|
|
361
|
+
sessionWorkDir = resolveSessionWorkDir({
|
|
362
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
363
|
+
harnessId: harness.harnessId,
|
|
364
|
+
sessionId,
|
|
365
|
+
workDir: sandboxBootstrapPlan.workDir,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// In case the sandbox session was created with a custom sandbox, or in
|
|
369
|
+
// case the sandbox provider doesn't respect `onFirstCreate`, we still
|
|
370
|
+
// have to ensure the harness bootstrap recipe has run. In the common
|
|
371
|
+
// scenario, this will be a cheap no-op based on just a marker check.
|
|
372
|
+
if (
|
|
373
|
+
sandboxBootstrapPlan.recipe != null &&
|
|
374
|
+
sandboxBootstrapPlan.recipeIdentity != null
|
|
375
|
+
) {
|
|
376
|
+
try {
|
|
377
|
+
await applyBootstrapRecipe({
|
|
378
|
+
session: sandboxSession.restricted(),
|
|
379
|
+
recipe: sandboxBootstrapPlan.recipe,
|
|
380
|
+
identity: sandboxBootstrapPlan.recipeIdentity,
|
|
381
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
382
|
+
abortSignal,
|
|
383
|
+
});
|
|
384
|
+
} catch (err) {
|
|
385
|
+
await cleanupAfterStartFailure({
|
|
386
|
+
sandboxSession,
|
|
387
|
+
ownsSandboxLifecycle,
|
|
388
|
+
});
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
330
392
|
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
331
396
|
await ensureSandboxDirectory({
|
|
332
397
|
session: sandboxSession,
|
|
333
398
|
workDir: sessionWorkDir,
|
|
@@ -342,10 +407,8 @@ export class HarnessAgent<
|
|
|
342
407
|
}
|
|
343
408
|
} catch (err) {
|
|
344
409
|
await cleanupAfterStartFailure({
|
|
345
|
-
sandboxProvider,
|
|
346
410
|
sandboxSession,
|
|
347
|
-
|
|
348
|
-
leasedBridgePort,
|
|
411
|
+
ownsSandboxLifecycle,
|
|
349
412
|
});
|
|
350
413
|
throw err;
|
|
351
414
|
}
|
|
@@ -371,8 +434,7 @@ export class HarnessAgent<
|
|
|
371
434
|
harness,
|
|
372
435
|
underlyingSession,
|
|
373
436
|
sandboxSession,
|
|
374
|
-
|
|
375
|
-
leasedBridgePort,
|
|
437
|
+
ownsSandboxLifecycle,
|
|
376
438
|
sessionWorkDir,
|
|
377
439
|
toolApproval: this.settings.toolApproval,
|
|
378
440
|
pendingToolApprovals: effectiveContinueFrom?.pendingToolApprovals,
|
|
@@ -390,10 +452,8 @@ export class HarnessAgent<
|
|
|
390
452
|
});
|
|
391
453
|
} catch (error) {
|
|
392
454
|
await cleanupAfterStartFailure({
|
|
393
|
-
sandboxProvider,
|
|
394
455
|
sandboxSession,
|
|
395
|
-
|
|
396
|
-
leasedBridgePort,
|
|
456
|
+
ownsSandboxLifecycle,
|
|
397
457
|
});
|
|
398
458
|
throw error;
|
|
399
459
|
}
|
|
@@ -590,34 +650,6 @@ export class HarnessAgent<
|
|
|
590
650
|
});
|
|
591
651
|
}
|
|
592
652
|
|
|
593
|
-
private async _acquireSandbox(input: {
|
|
594
|
-
sandboxProvider: HarnessV1SandboxProvider;
|
|
595
|
-
sessionId: string;
|
|
596
|
-
isResume: boolean;
|
|
597
|
-
bootstrapPlan: SandboxBootstrapPlan;
|
|
598
|
-
abortSignal: AbortSignal | undefined;
|
|
599
|
-
}): Promise<HarnessV1NetworkSandboxSession> {
|
|
600
|
-
const { sandboxProvider } = input;
|
|
601
|
-
if (input.isResume) {
|
|
602
|
-
if (sandboxProvider.resumeSession == null) {
|
|
603
|
-
throw new HarnessCapabilityUnsupportedError({
|
|
604
|
-
message: `Sandbox provider '${sandboxProvider.providerId}' does not support resume.`,
|
|
605
|
-
harnessId: this.settings.harness.harnessId,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
return sandboxProvider.resumeSession({
|
|
609
|
-
sessionId: input.sessionId,
|
|
610
|
-
abortSignal: input.abortSignal,
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
return sandboxProvider.createSession({
|
|
614
|
-
sessionId: input.sessionId,
|
|
615
|
-
abortSignal: input.abortSignal,
|
|
616
|
-
identity: input.bootstrapPlan.identity,
|
|
617
|
-
onFirstCreate: input.bootstrapPlan.onFirstCreate,
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
653
|
/*
|
|
622
654
|
* Reduce AI SDK input to the single user message the harness should run
|
|
623
655
|
* for this turn. The harness session owns prior-turn state (system
|
|
@@ -890,64 +922,10 @@ function resolveSandboxConfig(
|
|
|
890
922
|
};
|
|
891
923
|
}
|
|
892
924
|
|
|
893
|
-
/*
|
|
894
|
-
* Bridge-port leasing helper. Returns the port-narrowed network sandbox session
|
|
895
|
-
* plus the leased port (or `undefined` when the provider has no port pool). Kept here
|
|
896
|
-
* rather than on the session so the lease is established as part of session
|
|
897
|
-
* start — the session only needs to release it on close/detach.
|
|
898
|
-
*/
|
|
899
|
-
function applyPortLease(input: {
|
|
900
|
-
provider: HarnessV1SandboxProvider;
|
|
901
|
-
sandboxSession: HarnessV1NetworkSandboxSession;
|
|
902
|
-
sessionId: string;
|
|
903
|
-
}): {
|
|
904
|
-
sandboxSession: HarnessV1NetworkSandboxSession;
|
|
905
|
-
port: number | undefined;
|
|
906
|
-
} {
|
|
907
|
-
const pool = input.provider.bridgePorts;
|
|
908
|
-
if (pool == null || pool.length === 0) {
|
|
909
|
-
return { sandboxSession: input.sandboxSession, port: undefined };
|
|
910
|
-
}
|
|
911
|
-
const port = acquireBridgePort({
|
|
912
|
-
poolKey: input.provider,
|
|
913
|
-
pool,
|
|
914
|
-
sessionId: input.sessionId,
|
|
915
|
-
});
|
|
916
|
-
return {
|
|
917
|
-
sandboxSession: narrowNetworkSessionPorts(input.sandboxSession, port),
|
|
918
|
-
port,
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/*
|
|
923
|
-
* Derive a view of the network sandbox session that reports only the leased
|
|
924
|
-
* port. Implemented as a prototype-delegating overlay so every other member
|
|
925
|
-
* (file I/O, exec, spawn, lifecycle, `restricted`) forwards to the same live
|
|
926
|
-
* instance — only `ports` is shadowed.
|
|
927
|
-
*/
|
|
928
|
-
function narrowNetworkSessionPorts(
|
|
929
|
-
sandboxSession: HarnessV1NetworkSandboxSession,
|
|
930
|
-
leasedPort: number,
|
|
931
|
-
): HarnessV1NetworkSandboxSession {
|
|
932
|
-
return Object.create(sandboxSession, {
|
|
933
|
-
ports: {
|
|
934
|
-
value: [leasedPort] as ReadonlyArray<number>,
|
|
935
|
-
enumerable: true,
|
|
936
|
-
},
|
|
937
|
-
}) as HarnessV1NetworkSandboxSession;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
925
|
async function cleanupAfterStartFailure(input: {
|
|
941
|
-
sandboxProvider: HarnessV1SandboxProvider;
|
|
942
926
|
sandboxSession: HarnessV1NetworkSandboxSession;
|
|
943
|
-
|
|
944
|
-
leasedBridgePort: number | undefined;
|
|
927
|
+
ownsSandboxLifecycle: boolean;
|
|
945
928
|
}): Promise<void> {
|
|
929
|
+
if (!input.ownsSandboxLifecycle) return;
|
|
946
930
|
await Promise.resolve(input.sandboxSession.stop()).catch(() => {});
|
|
947
|
-
if (input.leasedBridgePort != null) {
|
|
948
|
-
releaseBridgePort({
|
|
949
|
-
poolKey: input.sandboxProvider,
|
|
950
|
-
sessionId: input.sessionId,
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
931
|
}
|
|
@@ -824,21 +824,10 @@ export class HarnessStreamTextResult<
|
|
|
824
824
|
return;
|
|
825
825
|
}
|
|
826
826
|
case 'tool-call':
|
|
827
|
-
this.currentStepContent.push({
|
|
828
|
-
...(part as object),
|
|
829
|
-
} as ContentPart<TOOLS>);
|
|
830
|
-
return;
|
|
831
827
|
case 'tool-approval-request':
|
|
832
|
-
this.currentStepContent.push({
|
|
833
|
-
...(part as object),
|
|
834
|
-
} as ContentPart<TOOLS>);
|
|
835
|
-
return;
|
|
836
828
|
case 'tool-approval-response':
|
|
837
|
-
this.currentStepContent.push({
|
|
838
|
-
...(part as object),
|
|
839
|
-
} as ContentPart<TOOLS>);
|
|
840
|
-
return;
|
|
841
829
|
case 'tool-result':
|
|
830
|
+
case 'tool-error':
|
|
842
831
|
this.currentStepContent.push({
|
|
843
832
|
...(part as object),
|
|
844
833
|
} as ContentPart<TOOLS>);
|
|
@@ -219,6 +219,19 @@ export function runPrompt<
|
|
|
219
219
|
string,
|
|
220
220
|
Extract<HarnessV1StreamPart, { type: 'tool-call' }>
|
|
221
221
|
>();
|
|
222
|
+
/*
|
|
223
|
+
* Host results are echoed back as `tool-result` events too, so the event
|
|
224
|
+
* alone cannot say who ran the tool — classify by the originating
|
|
225
|
+
* `tool-call`. Only an explicit `true` counts: `LanguageModelV4ToolCall`
|
|
226
|
+
* defines an omitted flag as client-executed. A call missing from the map
|
|
227
|
+
* arrived in an earlier slice, where nothing here can classify it.
|
|
228
|
+
*/
|
|
229
|
+
const translateOptions = {
|
|
230
|
+
isProviderExecuted: (toolCallId: string): boolean => {
|
|
231
|
+
const rawToolCall = rawToolCallsByToolCallId.get(toolCallId);
|
|
232
|
+
return rawToolCall == null || rawToolCall.providerExecuted === true;
|
|
233
|
+
},
|
|
234
|
+
};
|
|
222
235
|
const pendingApprovalsByApprovalId = new Map(
|
|
223
236
|
pendingToolApprovals.map(approval => [approval.approvalId, approval]),
|
|
224
237
|
);
|
|
@@ -709,7 +722,10 @@ export function runPrompt<
|
|
|
709
722
|
}
|
|
710
723
|
|
|
711
724
|
// Forward to consumer as soon as possible.
|
|
712
|
-
for (const part of translateStreamPart<TOOLS>(
|
|
725
|
+
for (const part of translateStreamPart<TOOLS>(
|
|
726
|
+
displayValue,
|
|
727
|
+
translateOptions,
|
|
728
|
+
)) {
|
|
713
729
|
result.enqueue(part);
|
|
714
730
|
}
|
|
715
731
|
|