@ai-sdk/harness 1.0.76 → 1.0.78

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # @ai-sdk/harness
2
2
 
3
+ ## 1.0.78
4
+
5
+ ### Patch Changes
6
+
7
+ - eace6fb: feat(harness): add experimental support for steering agent conversations mid-turn
8
+ - c0595b4: feat(harness): support passing a filesystem and process restricted sandbox session to `HarnessAgentSession`, using fallbacks in favor of the preferred network sandbox session methods
9
+
10
+ ## 1.0.77
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [e6087c9]
15
+ - Updated dependencies [9566914]
16
+ - Updated dependencies [b181020]
17
+ - Updated dependencies [7054073]
18
+ - Updated dependencies [a828527]
19
+ - Updated dependencies [d3cc3fe]
20
+ - @ai-sdk/provider-utils@5.0.28
21
+ - ai@7.0.70
22
+
3
23
  ## 1.0.76
4
24
 
5
25
  ### Patch Changes
package/bridge/index.ts CHANGED
@@ -10,4 +10,6 @@ export type {
10
10
  BridgeHandle,
11
11
  BridgeDebugConfig,
12
12
  BridgeDebugLevel,
13
+ Experimental_BridgeUserMessage,
14
+ Experimental_BridgeUserMessageQueue,
13
15
  } from '../src/bridge';
@@ -692,20 +692,16 @@ type HarnessV1StartOptions = {
692
692
  */
693
693
  readonly observability?: HarnessV1Observability;
694
694
  /**
695
- * Network sandbox session the adapter operates against. It is owned and
696
- * lifecycled by `HarnessAgent`. Adapters call `restricted()` for the
697
- * tool-safe filesystem/exec/spawn surface, and use the infra methods
698
- * (`getPortEndpoint`, `ports`, `setNetworkPolicy`,
699
- * `setRequestTransformations`, `addRequestTransformations`) for bridge
700
- * wiring. Adapters must not call `stop()` themselves; the agent does that
701
- * during cleanup.
695
+ * Sandbox session the adapter operates against. Network sandbox sessions
696
+ * expose optional infrastructure capabilities for bridge wiring; caller-
697
+ * provided basic sandbox sessions expose only filesystem and process APIs.
698
+ * Adapters must not stop or destroy the sandbox themselves.
702
699
  */
703
- readonly sandboxSession: HarnessV1NetworkSandboxSession;
700
+ readonly sandboxSession: HarnessV1NetworkSandboxSession | Experimental_SandboxSession;
704
701
  /**
705
- * Absolute path the adapter runs the agent in for this session. Composed by
706
- * the framework as `<sandboxSession.defaultWorkingDirectory>/<harnessId>-<sessionId>`
707
- * and created before `doStart`, so the adapter uses it directly instead of
708
- * deriving its own provider-specific path.
702
+ * Absolute path the adapter runs the agent in for this session. Composed
703
+ * underneath the sandbox's resolved default working directory and created
704
+ * before `doStart`.
709
705
  */
710
706
  readonly sessionWorkDir: string;
711
707
  };
@@ -1402,7 +1398,7 @@ type HarnessAgentTurnState = 'idle' | 'running' | 'awaiting-approval' | 'awaitin
1402
1398
  * Live harness session held by the caller.
1403
1399
  *
1404
1400
  * Created by {@link import('./harness-agent').HarnessAgent.createSession}.
1405
- * Owns the underlying adapter session and the network sandbox session.
1401
+ * Owns the underlying adapter session and holds its sandbox session.
1406
1402
  *
1407
1403
  * Pass the instance back to `agent.generate` / `agent.stream` on every
1408
1404
  * call; end the local handle with `detach()`, `stop()`, or `destroy()`.
@@ -1429,6 +1425,7 @@ declare class HarnessAgentSession {
1429
1425
  private turnState;
1430
1426
  private turnSequence;
1431
1427
  private activeTurnSequence;
1428
+ private activePromptControl;
1432
1429
  private suspendedTurnState;
1433
1430
  /**
1434
1431
  * Whether this session was created from `resumeFrom` or `continueFrom`.
@@ -1439,7 +1436,7 @@ declare class HarnessAgentSession {
1439
1436
  sessionId: string;
1440
1437
  harness: HarnessAgentAdapter;
1441
1438
  underlyingSession: HarnessAgentAdapterSession;
1442
- sandboxSession: HarnessV1NetworkSandboxSession;
1439
+ sandboxSession: HarnessV1NetworkSandboxSession | Experimental_SandboxSession;
1443
1440
  ownsSandboxLifecycle?: boolean;
1444
1441
  sessionWorkDir: string;
1445
1442
  toolApproval: HarnessAgentToolApprovalConfiguration | undefined;
@@ -1488,6 +1485,13 @@ declare class HarnessAgentSession {
1488
1485
  * the hood). Throws if the session has ended.
1489
1486
  */
1490
1487
  compact(customInstructions?: string): Promise<void>;
1488
+ /**
1489
+ * Submit another user message to the active turn.
1490
+ *
1491
+ * The runtime accepts the message for its next safe input boundary. Output
1492
+ * caused by the message remains part of the active turn's result stream.
1493
+ */
1494
+ experimental_steerTurn(text: string): Promise<void>;
1491
1495
  /**
1492
1496
  * Park the session, returning a payload the caller can persist and later
1493
1497
  * pass to `agent.createSession({ sessionId, resumeFrom })` to reconnect.
@@ -1531,6 +1535,9 @@ declare class HarnessAgentSession {
1531
1535
  private markAwaitingApprovalIfActive;
1532
1536
  private markAwaitingToolResultIfActive;
1533
1537
  private startTrackedTurn;
1538
+ private setPromptControl;
1539
+ private settleActivePromptControl;
1540
+ private clearActivePromptControl;
1534
1541
  private finishTrackedTurn;
1535
1542
  private endLocalHandle;
1536
1543
  private requireReusableSession;
@@ -1576,12 +1583,13 @@ interface HarnessAgentCallExtensions {
1576
1583
  * Adapter builtin tools (e.g. Claude Code's `Bash`) pass through
1577
1584
  * untouched.
1578
1585
  * - **Sandbox propagation.** On `createSession`, the agent uses a
1579
- * caller-provided network sandbox session when present; otherwise it calls
1580
- * the configured provider's `createSession()` (or `resumeSession()`). It
1581
- * passes the selected session into `doStart`. Its `restricted()` view (a tool-safe
1582
- * `Experimental_SandboxSession`) is handed to user-tool `execute()` calls
1583
- * via `experimental_sandbox`. Caller-provided sandboxes remain owned by the
1584
- * caller and are not stopped or destroyed by the harness layer.
1586
+ * caller-provided network or basic sandbox session when present; otherwise
1587
+ * it calls the configured provider's `createSession()` (or
1588
+ * `resumeSession()`). It passes the selected session into `doStart`. A
1589
+ * tool-safe `SandboxSession` is handed to user-tool
1590
+ * `execute()` calls via `experimental_sandbox`. Caller-provided sandboxes
1591
+ * remain owned by the caller and are not stopped or destroyed by the
1592
+ * harness layer.
1585
1593
  */
1586
1594
  declare class HarnessAgent<THarness extends HarnessAgentAdapter<any> = HarnessAgentAdapter, TUserTools extends ToolSet = {}, RUNTIME_CONTEXT extends Context = Context, OUTPUT extends OutputInterface = never> implements Agent<never, HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT, OUTPUT> {
1587
1595
  readonly version: "agent-v1";
@@ -1631,10 +1639,10 @@ declare class HarnessAgent<THarness extends HarnessAgentAdapter<any> = HarnessAg
1631
1639
  */
1632
1640
  continueFrom?: HarnessAgentContinueTurnState;
1633
1641
  /**
1634
- * Existing network sandbox session to run the harness in. When provided,
1635
- * the caller retains ownership of the sandbox lifecycle.
1642
+ * Existing sandbox session to run the harness in. When provided, the
1643
+ * caller retains ownership of the sandbox lifecycle.
1636
1644
  */
1637
- sandboxSession?: HarnessV1NetworkSandboxSession;
1645
+ sandboxSession?: HarnessV1NetworkSandboxSession | Experimental_SandboxSession;
1638
1646
  abortSignal?: AbortSignal;
1639
1647
  }): Promise<HarnessAgentSession>;
1640
1648
  generate(options: AgentCallParameters<never, HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT> & HarnessAgentCallExtensions): Promise<GenerateTextResult<HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT, OUTPUT>>;
@@ -1664,6 +1672,17 @@ declare class HarnessAgent<THarness extends HarnessAgentAdapter<any> = HarnessAg
1664
1672
  toolResultContinuations?: readonly HarnessAgentToolResultContinuation[];
1665
1673
  abortSignal?: AbortSignal;
1666
1674
  }): Promise<StreamTextResult<HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT, OUTPUT>>;
1675
+ /**
1676
+ * Submit another user message to a currently running session turn.
1677
+ *
1678
+ * The returned promise resolves after the runtime has accepted the message
1679
+ * for its next safe input boundary. Output caused by the message remains in
1680
+ * the current turn's stream.
1681
+ */
1682
+ experimental_steer(options: {
1683
+ session: HarnessAgentSession;
1684
+ text: string;
1685
+ }): Promise<void>;
1667
1686
  private _startTurn;
1668
1687
  private _resolveTurnInput;
1669
1688
  private _toToolSpecs;
@@ -1511,7 +1511,7 @@ function runPrompt(input) {
1511
1511
  result.fail(err);
1512
1512
  };
1513
1513
  const done = (async () => {
1514
- var _a5, _b5, _c2, _d2, _e2, _f2, _g2, _h, _i, _j, _k, _l;
1514
+ var _a5, _b5, _c2, _d2, _e2, _f2, _g2, _h, _i, _j, _k, _l, _m;
1515
1515
  let bridge;
1516
1516
  try {
1517
1517
  bridge = await toHarnessStream({
@@ -1549,6 +1549,7 @@ function runPrompt(input) {
1549
1549
  return;
1550
1550
  }
1551
1551
  const { stream, control } = bridge;
1552
+ (_a5 = input.onPromptControlAvailable) == null ? void 0 : _a5.call(input, control);
1552
1553
  const reader = stream.getReader();
1553
1554
  const toolCallsByToolCallId = /* @__PURE__ */ new Map();
1554
1555
  const rawToolCallsByToolCallId = /* @__PURE__ */ new Map();
@@ -1565,7 +1566,7 @@ function runPrompt(input) {
1565
1566
  pendingToolApprovals.map((approval) => [approval.toolCallId, approval])
1566
1567
  );
1567
1568
  const continuationsByApprovalId = new Map(
1568
- ((_a5 = input.toolApprovalContinuations) != null ? _a5 : []).map((continuation) => [
1569
+ ((_b5 = input.toolApprovalContinuations) != null ? _b5 : []).map((continuation) => [
1569
1570
  continuation.approvalResponse.approvalId,
1570
1571
  continuation
1571
1572
  ])
@@ -1577,7 +1578,7 @@ function runPrompt(input) {
1577
1578
  ])
1578
1579
  );
1579
1580
  const continuationsByToolCallId = new Map(
1580
- ((_b5 = input.toolResultContinuations) != null ? _b5 : []).map((continuation) => [
1581
+ ((_c2 = input.toolResultContinuations) != null ? _c2 : []).map((continuation) => [
1581
1582
  continuation.toolCallId,
1582
1583
  continuation
1583
1584
  ])
@@ -1870,7 +1871,7 @@ function runPrompt(input) {
1870
1871
  (condition) => condition({ steps: completedSteps })
1871
1872
  )
1872
1873
  )).some(Boolean)) {
1873
- await ((_c2 = input.onStopConditionMet) == null ? void 0 : _c2.call(input));
1874
+ await ((_d2 = input.onStopConditionMet) == null ? void 0 : _d2.call(input));
1874
1875
  const { finishReason, usage } = pendingStopBoundary;
1875
1876
  releasePendingStopBoundary();
1876
1877
  await telemetry.end({ finishReason, usage });
@@ -1881,7 +1882,7 @@ function runPrompt(input) {
1881
1882
  }
1882
1883
  }
1883
1884
  if (value.type === "stream-start") {
1884
- await telemetry.start((_d2 = value.modelId) != null ? _d2 : input.session.modelId);
1885
+ await telemetry.start((_e2 = value.modelId) != null ? _e2 : input.session.modelId);
1885
1886
  }
1886
1887
  if (value.type !== "stream-start" && value.type !== "finish-step" && value.type !== "finish" && value.type !== "error") {
1887
1888
  await telemetry.ensureStepOpen();
@@ -1907,7 +1908,7 @@ function runPrompt(input) {
1907
1908
  const rawToolCall = rawToolCallsByToolCallId.get(
1908
1909
  displayValue.toolCallId
1909
1910
  );
1910
- const toolName = (_e2 = rawToolCall == null ? void 0 : rawToolCall.toolName) != null ? _e2 : toolCall.toolName;
1911
+ const toolName = (_f2 = rawToolCall == null ? void 0 : rawToolCall.toolName) != null ? _f2 : toolCall.toolName;
1911
1912
  if (!isHarnessV1BuiltinToolIncluded({
1912
1913
  toolName,
1913
1914
  toolFiltering: input.builtinToolFiltering
@@ -1987,13 +1988,13 @@ function runPrompt(input) {
1987
1988
  );
1988
1989
  }
1989
1990
  const rawToolCall = rawToolCallsByToolCallId.get(value.toolCallId);
1990
- const pendingApproval = (_h = pendingApprovalsByApprovalId.get(value.approvalId)) != null ? _h : {
1991
+ const pendingApproval = (_i = pendingApprovalsByApprovalId.get(value.approvalId)) != null ? _i : {
1991
1992
  approvalId: value.approvalId,
1992
1993
  toolCallId: value.toolCallId,
1993
1994
  toolName: toolCall.toolName,
1994
- input: (_f2 = rawToolCall == null ? void 0 : rawToolCall.input) != null ? _f2 : JSON.stringify(toolCall.input),
1995
+ input: (_g2 = rawToolCall == null ? void 0 : rawToolCall.input) != null ? _g2 : JSON.stringify(toolCall.input),
1995
1996
  kind: "builtin",
1996
- providerExecuted: (_g2 = rawToolCall == null ? void 0 : rawToolCall.providerExecuted) != null ? _g2 : true,
1997
+ providerExecuted: (_h = rawToolCall == null ? void 0 : rawToolCall.providerExecuted) != null ? _h : true,
1997
1998
  ...(rawToolCall == null ? void 0 : rawToolCall.nativeName) !== void 0 ? { nativeName: rawToolCall.nativeName } : {}
1998
1999
  };
1999
2000
  pendingApprovalsByApprovalId.set(
@@ -2098,7 +2099,7 @@ function runPrompt(input) {
2098
2099
  await telemetry.toolEnd(toolCall.toolCallId, { ok: true, output });
2099
2100
  continue;
2100
2101
  }
2101
- const pendingApproval = (_i = pendingApprovalsByToolCallId.get(toolCall.toolCallId)) != null ? _i : customToolApprovalDecision.type === "request" ? {
2102
+ const pendingApproval = (_j = pendingApprovalsByToolCallId.get(toolCall.toolCallId)) != null ? _j : customToolApprovalDecision.type === "request" ? {
2102
2103
  approvalId: generateId4(),
2103
2104
  toolCallId: toolCall.toolCallId,
2104
2105
  toolName: toolCall.toolName,
@@ -2189,15 +2190,15 @@ function runPrompt(input) {
2189
2190
  }
2190
2191
  }
2191
2192
  await waitForOutstandingHostToolExecutions();
2192
- const isTurnSuspending = ((_j = input.isTurnSuspending) == null ? void 0 : _j.call(input)) === true;
2193
+ const isTurnSuspending = ((_k = input.isTurnSuspending) == null ? void 0 : _k.call(input)) === true;
2193
2194
  if (isTurnSuspending) {
2194
2195
  if (finalFinish == null) {
2195
2196
  result.discardCurrentStepContent();
2196
2197
  }
2197
2198
  } else if (finalFinish != null) {
2198
- (_k = input.onTurnFinished) == null ? void 0 : _k.call(input);
2199
+ (_l = input.onTurnFinished) == null ? void 0 : _l.call(input);
2199
2200
  } else {
2200
- (_l = input.onTurnFailed) == null ? void 0 : _l.call(input);
2201
+ (_m = input.onTurnFailed) == null ? void 0 : _m.call(input);
2201
2202
  }
2202
2203
  await result.finish(
2203
2204
  finalFinish ? {
@@ -2317,6 +2318,11 @@ function promptToText(prompt) {
2317
2318
  return "";
2318
2319
  }
2319
2320
 
2321
+ // src/utils/get-restricted-sandbox-session.ts
2322
+ function getRestrictedSandboxSession(sandboxSession) {
2323
+ return "restricted" in sandboxSession ? sandboxSession.restricted() : sandboxSession;
2324
+ }
2325
+
2320
2326
  // src/agent/harness-agent-session.ts
2321
2327
  var HarnessAgentSession = class {
2322
2328
  constructor(options) {
@@ -2343,7 +2349,7 @@ var HarnessAgentSession = class {
2343
2349
  this.isResume = options.underlyingSession.isResume;
2344
2350
  }
2345
2351
  /**
2346
- * Active network sandbox session.
2352
+ * Active sandbox session.
2347
2353
  *
2348
2354
  * @internal — accessed by session turn and lifecycle drivers.
2349
2355
  */
@@ -2382,7 +2388,7 @@ var HarnessAgentSession = class {
2382
2388
  activeTools: options.activeTools,
2383
2389
  toolSpecs: options.toolSpecs,
2384
2390
  builtinToolFiltering: options.builtinToolFiltering,
2385
- sandboxSession: sandboxSession.restricted(),
2391
+ sandboxSession: getRestrictedSandboxSession(sandboxSession),
2386
2392
  sessionWorkDir: this.sessionWorkDir,
2387
2393
  runtimeContext: options.runtimeContext,
2388
2394
  abortSignal: options.abortSignal,
@@ -2413,6 +2419,9 @@ var HarnessAgentSession = class {
2413
2419
  onTurnFailed: () => {
2414
2420
  this.finishTrackedTurn({ turnId });
2415
2421
  },
2422
+ onPromptControlAvailable: (control) => {
2423
+ this.setPromptControl({ turnId, control });
2424
+ },
2416
2425
  isTurnSuspending: () => this.activeTurnSequence === turnId && this.suspendedTurnState != null,
2417
2426
  onStopConditionMet: () => this.captureStopConditionBoundary({ session, turnId })
2418
2427
  });
@@ -2437,7 +2446,7 @@ var HarnessAgentSession = class {
2437
2446
  activeTools: options.activeTools,
2438
2447
  toolSpecs: options.toolSpecs,
2439
2448
  builtinToolFiltering: options.builtinToolFiltering,
2440
- sandboxSession: sandboxSession.restricted(),
2449
+ sandboxSession: getRestrictedSandboxSession(sandboxSession),
2441
2450
  sessionWorkDir: this.sessionWorkDir,
2442
2451
  runtimeContext: options.runtimeContext,
2443
2452
  abortSignal: options.abortSignal,
@@ -2470,6 +2479,9 @@ var HarnessAgentSession = class {
2470
2479
  onTurnFailed: () => {
2471
2480
  this.finishTrackedTurn({ turnId });
2472
2481
  },
2482
+ onPromptControlAvailable: (control) => {
2483
+ this.setPromptControl({ turnId, control });
2484
+ },
2473
2485
  isTurnSuspending: () => this.activeTurnSequence === turnId && this.suspendedTurnState != null,
2474
2486
  onStopConditionMet: () => this.captureStopConditionBoundary({ session, turnId })
2475
2487
  });
@@ -2492,6 +2504,34 @@ var HarnessAgentSession = class {
2492
2504
  async compact(customInstructions) {
2493
2505
  await this.requireReusableSession().doCompact(customInstructions);
2494
2506
  }
2507
+ /**
2508
+ * Submit another user message to the active turn.
2509
+ *
2510
+ * The runtime accepts the message for its next safe input boundary. Output
2511
+ * caused by the message remains part of the active turn's result stream.
2512
+ */
2513
+ async experimental_steerTurn(text) {
2514
+ this.requireReusableSession();
2515
+ const activePromptControl = this.activePromptControl;
2516
+ if (this.turnState !== "running" || this.suspendedTurnState != null || activePromptControl == null) {
2517
+ throw new Error(
2518
+ `Harness session ${this.sessionId} has no running turn to steer.`
2519
+ );
2520
+ }
2521
+ const control = await activePromptControl.promise;
2522
+ if (control == null || this.sessionState !== "active" || this.turnState !== "running" || this.suspendedTurnState != null || this.activePromptControl !== activePromptControl || this.activeTurnSequence !== activePromptControl.turnId) {
2523
+ throw new Error(
2524
+ `Harness session ${this.sessionId} no longer has the running turn targeted for steering.`
2525
+ );
2526
+ }
2527
+ if (control.submitUserMessage == null) {
2528
+ throw new HarnessCapabilityUnsupportedError({
2529
+ message: `Harness '${this.harness.harnessId}' does not support steering active turns.`,
2530
+ harnessId: this.harness.harnessId
2531
+ });
2532
+ }
2533
+ await control.submitUserMessage(text);
2534
+ }
2495
2535
  /**
2496
2536
  * Park the session, returning a payload the caller can persist and later
2497
2537
  * pass to `agent.createSession({ sessionId, resumeFrom })` to reconnect.
@@ -2551,7 +2591,7 @@ var HarnessAgentSession = class {
2551
2591
  return validated;
2552
2592
  } finally {
2553
2593
  this.endLocalHandle({ sessionState: "stopped" });
2554
- if (this.ownsSandboxLifecycle) {
2594
+ if (this.ownsSandboxLifecycle && "stop" in sandboxSession) {
2555
2595
  await Promise.resolve(sandboxSession.stop()).catch(() => {
2556
2596
  });
2557
2597
  }
@@ -2572,10 +2612,12 @@ var HarnessAgentSession = class {
2572
2612
  });
2573
2613
  }
2574
2614
  if (!this.ownsSandboxLifecycle) return;
2575
- await Promise.resolve(
2576
- (_b4 = (_a4 = sandboxSession.destroy) == null ? void 0 : _a4.call(sandboxSession)) != null ? _b4 : sandboxSession.stop()
2577
- ).catch(() => {
2578
- });
2615
+ if ("stop" in sandboxSession) {
2616
+ await Promise.resolve(
2617
+ (_b4 = (_a4 = sandboxSession.destroy) == null ? void 0 : _a4.call(sandboxSession)) != null ? _b4 : sandboxSession.stop()
2618
+ ).catch(() => {
2619
+ });
2620
+ }
2579
2621
  }
2580
2622
  /**
2581
2623
  * Gracefully freeze the active turn at the slice boundary and return the
@@ -2632,6 +2674,7 @@ var HarnessAgentSession = class {
2632
2674
  }
2633
2675
  async suspendCurrentTurn(options) {
2634
2676
  var _a4;
2677
+ this.clearActivePromptControl();
2635
2678
  (_a4 = this.suspendedTurnState) != null ? _a4 : this.suspendedTurnState = (async () => {
2636
2679
  const raw = await options.session.doSuspendTurn();
2637
2680
  const validated = await validateLifecycleStateData({
@@ -2687,11 +2730,13 @@ var HarnessAgentSession = class {
2687
2730
  }
2688
2731
  markAwaitingApprovalIfActive() {
2689
2732
  if (this.sessionState === "active") {
2733
+ this.clearActivePromptControl();
2690
2734
  this.turnState = "awaiting-approval";
2691
2735
  }
2692
2736
  }
2693
2737
  markAwaitingToolResultIfActive() {
2694
2738
  if (this.sessionState === "active") {
2739
+ this.clearActivePromptControl();
2695
2740
  this.turnState = "awaiting-tool-result";
2696
2741
  }
2697
2742
  }
@@ -2700,17 +2745,52 @@ var HarnessAgentSession = class {
2700
2745
  this.activeTurnSequence = turnId;
2701
2746
  this.suspendedTurnState = void 0;
2702
2747
  this.turnState = "running";
2748
+ this.clearActivePromptControl();
2749
+ let resolve;
2750
+ const promise = new Promise(
2751
+ (resolvePromise) => {
2752
+ resolve = resolvePromise;
2753
+ }
2754
+ );
2755
+ this.activePromptControl = {
2756
+ turnId,
2757
+ promise,
2758
+ resolve,
2759
+ settled: false
2760
+ };
2703
2761
  return turnId;
2704
2762
  }
2763
+ setPromptControl(options) {
2764
+ const activePromptControl = this.activePromptControl;
2765
+ if (this.sessionState !== "active" || this.turnState !== "running" || this.activeTurnSequence !== options.turnId || (activePromptControl == null ? void 0 : activePromptControl.turnId) !== options.turnId) {
2766
+ return;
2767
+ }
2768
+ this.settleActivePromptControl(options.control);
2769
+ }
2770
+ settleActivePromptControl(control) {
2771
+ const activePromptControl = this.activePromptControl;
2772
+ if (activePromptControl == null || activePromptControl.settled) return;
2773
+ activePromptControl.settled = true;
2774
+ activePromptControl.resolve(control);
2775
+ }
2776
+ clearActivePromptControl(turnId) {
2777
+ if (turnId != null && this.activePromptControl != null && this.activePromptControl.turnId !== turnId) {
2778
+ return;
2779
+ }
2780
+ this.settleActivePromptControl(void 0);
2781
+ this.activePromptControl = void 0;
2782
+ }
2705
2783
  finishTrackedTurn(options) {
2706
2784
  if (this.sessionState !== "active") return;
2707
2785
  if (this.activeTurnSequence !== options.turnId) return;
2786
+ this.clearActivePromptControl(options.turnId);
2708
2787
  this.pendingToolApprovals.clear();
2709
2788
  this.pendingToolResults.clear();
2710
2789
  this.suspendedTurnState = void 0;
2711
2790
  this.turnState = "idle";
2712
2791
  }
2713
2792
  endLocalHandle(options) {
2793
+ this.clearActivePromptControl();
2714
2794
  this.sessionState = options.sessionState;
2715
2795
  this.underlyingSession = void 0;
2716
2796
  this.sandboxSession = void 0;
@@ -2942,7 +3022,36 @@ function resolveBootstrapPath({
2942
3022
  }
2943
3023
 
2944
3024
  // src/agent/internal/sandbox-bootstrap.ts
3025
+ import { posix as posix3 } from "path";
3026
+
3027
+ // src/utils/resolve-sandbox-default-working-directory.ts
2945
3028
  import { posix as posix2 } from "path";
3029
+ async function resolveSandboxDefaultWorkingDirectory({
3030
+ sandboxSession,
3031
+ abortSignal
3032
+ }) {
3033
+ if ("defaultWorkingDirectory" in sandboxSession) {
3034
+ return sandboxSession.defaultWorkingDirectory;
3035
+ }
3036
+ const result = await sandboxSession.run({
3037
+ command: "pwd",
3038
+ abortSignal
3039
+ });
3040
+ if (result.exitCode !== 0) {
3041
+ throw new Error(
3042
+ `Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`
3043
+ );
3044
+ }
3045
+ const cwd = result.stdout.trim();
3046
+ if (!posix2.isAbsolute(cwd)) {
3047
+ throw new Error(
3048
+ `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
3049
+ );
3050
+ }
3051
+ return cwd === "/" ? cwd : cwd.replace(/\/+$/, "");
3052
+ }
3053
+
3054
+ // src/agent/internal/sandbox-bootstrap.ts
2946
3055
  var SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
2947
3056
  function validateSandboxBootstrapSettings(settings) {
2948
3057
  if (settings.onBootstrap == null !== (settings.bootstrapHash == null)) {
@@ -2968,10 +3077,10 @@ function normalizeSandboxWorkDir(workDir) {
2968
3077
  "HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators."
2969
3078
  );
2970
3079
  }
2971
- if (posix2.isAbsolute(workDir)) {
3080
+ if (posix3.isAbsolute(workDir)) {
2972
3081
  throw new Error("HarnessAgent: `sandboxConfig.workDir` must be relative.");
2973
3082
  }
2974
- const normalized = posix2.normalize(workDir);
3083
+ const normalized = posix3.normalize(workDir);
2975
3084
  if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
2976
3085
  throw new Error(
2977
3086
  "HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory."
@@ -3030,8 +3139,8 @@ async function runSandboxBootstrap({
3030
3139
  abortSignal
3031
3140
  }) {
3032
3141
  if (recipe == null && onBootstrap == null) return;
3033
- const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveDefaultWorkingDirectory({
3034
- session,
3142
+ const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveSandboxDefaultWorkingDirectory({
3143
+ sandboxSession: session,
3035
3144
  abortSignal
3036
3145
  });
3037
3146
  if (recipe != null && recipeIdentity != null) {
@@ -3055,27 +3164,6 @@ async function runSandboxBootstrap({
3055
3164
  });
3056
3165
  await onBootstrap({ session, workDir: bootstrapWorkDir, abortSignal });
3057
3166
  }
3058
- async function resolveDefaultWorkingDirectory({
3059
- session,
3060
- abortSignal
3061
- }) {
3062
- const result = await session.run({
3063
- command: "pwd",
3064
- abortSignal
3065
- });
3066
- if (result.exitCode !== 0) {
3067
- throw new Error(
3068
- `Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`
3069
- );
3070
- }
3071
- const cwd = result.stdout.trim();
3072
- if (!posix2.isAbsolute(cwd)) {
3073
- throw new Error(
3074
- `Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
3075
- );
3076
- }
3077
- return cwd === "/" ? cwd : cwd.replace(/\/+$/, "");
3078
- }
3079
3167
  async function ensureSandboxDirectory({
3080
3168
  session,
3081
3169
  workDir,
@@ -3126,7 +3214,7 @@ function joinSandboxPath({
3126
3214
  base,
3127
3215
  path
3128
3216
  }) {
3129
- return posix2.join(base, path);
3217
+ return posix3.join(base, path);
3130
3218
  }
3131
3219
 
3132
3220
  // src/agent/internal/resolve-observability.ts
@@ -3347,8 +3435,13 @@ var HarnessAgent = class {
3347
3435
  let sessionWorkDir;
3348
3436
  if (providedSandboxSession != null) {
3349
3437
  sandboxSession = providedSandboxSession;
3438
+ const toolSafeSandboxSession = getRestrictedSandboxSession(sandboxSession);
3439
+ const defaultWorkingDirectory = await resolveSandboxDefaultWorkingDirectory({
3440
+ sandboxSession,
3441
+ abortSignal
3442
+ });
3350
3443
  sessionWorkDir = resolveSessionWorkDir({
3351
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
3444
+ defaultWorkingDirectory,
3352
3445
  harnessId: harness.harnessId,
3353
3446
  sessionId,
3354
3447
  workDir: this.sandboxConfig.workDir
@@ -3358,10 +3451,10 @@ var HarnessAgent = class {
3358
3451
  const recipeIdentity = await hashHarnessBootstrap(recipe);
3359
3452
  try {
3360
3453
  await applyBootstrapRecipe({
3361
- session: sandboxSession.restricted(),
3454
+ session: toolSafeSandboxSession,
3362
3455
  recipe,
3363
3456
  identity: recipeIdentity,
3364
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
3457
+ defaultWorkingDirectory,
3365
3458
  abortSignal
3366
3459
  });
3367
3460
  } catch (err) {
@@ -3385,12 +3478,13 @@ var HarnessAgent = class {
3385
3478
  harnessId: harness.harnessId
3386
3479
  });
3387
3480
  }
3388
- sandboxSession = await sandboxProvider.resumeSession({
3481
+ const resumedSandboxSession = await sandboxProvider.resumeSession({
3389
3482
  sessionId,
3390
3483
  abortSignal
3391
3484
  });
3485
+ sandboxSession = resumedSandboxSession;
3392
3486
  sessionWorkDir = resolveSessionWorkDir({
3393
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
3487
+ defaultWorkingDirectory: resumedSandboxSession.defaultWorkingDirectory,
3394
3488
  harnessId: harness.harnessId,
3395
3489
  sessionId,
3396
3490
  workDir: this.sandboxConfig.workDir
@@ -3404,14 +3498,15 @@ var HarnessAgent = class {
3404
3498
  recipe,
3405
3499
  settings: this.sandboxConfig
3406
3500
  });
3407
- sandboxSession = await sandboxProvider.createSession({
3501
+ const createdSandboxSession = await sandboxProvider.createSession({
3408
3502
  sessionId,
3409
3503
  abortSignal,
3410
3504
  identity: sandboxBootstrapPlan.identity,
3411
3505
  onFirstCreate: sandboxBootstrapPlan.onFirstCreate
3412
3506
  });
3507
+ sandboxSession = createdSandboxSession;
3413
3508
  sessionWorkDir = resolveSessionWorkDir({
3414
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
3509
+ defaultWorkingDirectory: createdSandboxSession.defaultWorkingDirectory,
3415
3510
  harnessId: harness.harnessId,
3416
3511
  sessionId,
3417
3512
  workDir: sandboxBootstrapPlan.workDir
@@ -3419,10 +3514,10 @@ var HarnessAgent = class {
3419
3514
  if (sandboxBootstrapPlan.recipe != null && sandboxBootstrapPlan.recipeIdentity != null) {
3420
3515
  try {
3421
3516
  await applyBootstrapRecipe({
3422
- session: sandboxSession.restricted(),
3517
+ session: createdSandboxSession.restricted(),
3423
3518
  recipe: sandboxBootstrapPlan.recipe,
3424
3519
  identity: sandboxBootstrapPlan.recipeIdentity,
3425
- defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
3520
+ defaultWorkingDirectory: createdSandboxSession.defaultWorkingDirectory,
3426
3521
  abortSignal
3427
3522
  });
3428
3523
  } catch (err) {
@@ -3443,7 +3538,7 @@ var HarnessAgent = class {
3443
3538
  });
3444
3539
  if (this.sandboxConfig.onSession != null) {
3445
3540
  await this.sandboxConfig.onSession({
3446
- session: sandboxSession.restricted(),
3541
+ session: getRestrictedSandboxSession(sandboxSession),
3447
3542
  sessionWorkDir,
3448
3543
  abortSignal
3449
3544
  });
@@ -3566,6 +3661,16 @@ var HarnessAgent = class {
3566
3661
  });
3567
3662
  return result;
3568
3663
  }
3664
+ /**
3665
+ * Submit another user message to a currently running session turn.
3666
+ *
3667
+ * The returned promise resolves after the runtime has accepted the message
3668
+ * for its next safe input boundary. Output caused by the message remains in
3669
+ * the current turn's stream.
3670
+ */
3671
+ async experimental_steer(options) {
3672
+ await options.session.experimental_steerTurn(options.text);
3673
+ }
3569
3674
  // ─── Internals ──────────────────────────────────────────────────────
3570
3675
  _startTurn(input) {
3571
3676
  if (input.turnInput.mode === "continue") {
@@ -3768,8 +3873,10 @@ function resolveSandboxConfig(settings) {
3768
3873
  }
3769
3874
  async function cleanupAfterStartFailure(input) {
3770
3875
  if (!input.ownsSandboxLifecycle) return;
3771
- await Promise.resolve(input.sandboxSession.stop()).catch(() => {
3772
- });
3876
+ if ("stop" in input.sandboxSession) {
3877
+ await Promise.resolve(input.sandboxSession.stop()).catch(() => {
3878
+ });
3879
+ }
3773
3880
  }
3774
3881
 
3775
3882
  // src/agent/prepare-harness-sandbox-template.ts
@@ -3841,8 +3948,8 @@ async function prepareSandboxForHarness(options) {
3841
3948
  }
3842
3949
  const recipeIdentity = await hashHarnessBootstrap(recipe);
3843
3950
  recipeIdentities[harness.harnessId] = recipeIdentity;
3844
- defaultWorkingDirectory != null ? defaultWorkingDirectory : defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
3845
- session: options.session,
3951
+ defaultWorkingDirectory != null ? defaultWorkingDirectory : defaultWorkingDirectory = await resolveSandboxDefaultWorkingDirectory({
3952
+ sandboxSession: options.session,
3846
3953
  abortSignal: options.abortSignal
3847
3954
  });
3848
3955
  await applyBootstrapRecipe({