@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/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.74",
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.67"
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.67",
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, the network sandbox session, and the
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
- sandboxProvider: HarnessV1SandboxProvider;
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.sandboxProvider = options.sandboxProvider;
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 sandbox.
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
- sessionState: 'stopped',
401
- releasePortLease: true,
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. The sandbox is destroyed when
409
- * the provider supports destruction; otherwise it is stopped.
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', releasePortLease: true });
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 and no port lease is
434
- * released, because bridge-backed adapters may still have a live bridge on
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
- * Sandbox provider whose `create()` produces the network sandbox session the
183
- * harness runs against. Its `restricted()` view is also propagated to user
184
- * tool `execute()` calls (as the `experimental_sandbox` field), typed as
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: HarnessV1SandboxProvider;
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
- acquireBridgePort,
54
- releaseBridgePort,
55
- } from './internal/bridge-port-registry';
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.** `settings.sandbox` is a sandbox provider.
114
- * On `createSession`, the agent calls `provider.createSession()` (or
115
- * `resumeSession()`) and passes the resulting network sandbox session into
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
- const acquiredSandboxSession = await this._acquireSandbox({
292
- sandboxProvider,
293
- sessionId,
294
- isResume: isResumedSession,
295
- bootstrapPlan: sandboxBootstrapPlan,
296
- abortSignal,
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
- const leased = applyPortLease({
300
- provider: sandboxProvider,
301
- sandboxSession: acquiredSandboxSession,
302
- sessionId,
303
- });
304
- const sandboxSession = leased.sandboxSession;
305
- const leasedBridgePort = leased.port;
306
- const sessionWorkDir = resolveSessionWorkDir({
307
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
308
- harnessId: harness.harnessId,
309
- sessionId,
310
- workDir: sandboxBootstrapPlan.workDir,
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
- try {
314
- // In case the sandbox session was created with a custom sandbox, or in
315
- // case the sandbox provider doesn't respect `onFirstCreate`, we still
316
- // have to ensure the harness bootstrap recipe has run. In the common
317
- // scenario, this will be a cheap no-op based on just a marker check.
318
- if (
319
- !isResumedSession &&
320
- sandboxBootstrapPlan.recipe != null &&
321
- sandboxBootstrapPlan.recipeIdentity != null
322
- ) {
323
- await applyBootstrapRecipe({
324
- session: sandboxSession.restricted(),
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
- sessionId,
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
- sandboxProvider,
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
- sessionId,
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
- sessionId: string;
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>(displayValue)) {
725
+ for (const part of translateStreamPart<TOOLS>(
726
+ displayValue,
727
+ translateOptions,
728
+ )) {
713
729
  result.enqueue(part);
714
730
  }
715
731