@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 +20 -0
- package/bridge/index.ts +2 -0
- package/dist/agent/index.d.ts +42 -23
- package/dist/agent/index.js +168 -61
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.d.ts +12 -7
- package/dist/bridge/index.js +140 -5
- package/dist/bridge/index.js.map +1 -1
- package/dist/index.d.ts +38 -13
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +177 -1
- package/dist/utils/index.js +121 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +4 -4
- package/src/agent/harness-agent-session.ts +136 -12
- package/src/agent/harness-agent.ts +54 -22
- package/src/agent/internal/run-prompt.ts +2 -0
- package/src/agent/internal/sandbox-bootstrap.ts +3 -28
- package/src/agent/prepare-sandbox-for-harness.ts +3 -3
- package/src/bridge/index.ts +184 -12
- package/src/utils/bridge-user-message-submitter.ts +96 -0
- package/src/utils/get-restricted-sandbox-session.ts +10 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/resolve-sandbox-default-working-directory.ts +33 -0
- package/src/utils/sandbox-channel.ts +9 -0
- package/src/v1/harness-v1-bridge-protocol.ts +23 -0
- package/src/v1/harness-v1-session.ts +9 -12
- package/src/v1/index.ts +4 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Context,
|
|
3
|
+
Experimental_SandboxSession as SandboxSession,
|
|
4
|
+
ToolSet,
|
|
5
|
+
} from '@ai-sdk/provider-utils';
|
|
2
6
|
import type {
|
|
3
7
|
OutputInterface as Output,
|
|
4
8
|
StopCondition,
|
|
@@ -9,8 +13,10 @@ import type { HarnessAgentToolApprovalConfiguration } from './harness-agent-sett
|
|
|
9
13
|
import type {
|
|
10
14
|
HarnessV1BuiltinToolFiltering,
|
|
11
15
|
HarnessV1NetworkSandboxSession,
|
|
16
|
+
HarnessV1PromptControl,
|
|
12
17
|
HarnessV1ResponseFormat,
|
|
13
18
|
} from '../v1';
|
|
19
|
+
import { HarnessCapabilityUnsupportedError } from '../errors/harness-capability-unsupported-error';
|
|
14
20
|
import type {
|
|
15
21
|
HarnessAgentAdapter,
|
|
16
22
|
HarnessAgentAdapterSession,
|
|
@@ -25,6 +31,7 @@ import type { HarnessAgentToolApprovalContinuation } from './harness-agent-tool-
|
|
|
25
31
|
import type { HarnessAgentToolResultContinuation } from './harness-agent-tool-result-continuation';
|
|
26
32
|
import { validateLifecycleStateData } from './internal/lifecycle-state-validation';
|
|
27
33
|
import { runPrompt } from './internal/run-prompt';
|
|
34
|
+
import { getRestrictedSandboxSession } from '../utils/get-restricted-sandbox-session';
|
|
28
35
|
|
|
29
36
|
type HarnessAgentTurnResult<
|
|
30
37
|
TOOLS extends ToolSet,
|
|
@@ -44,11 +51,18 @@ type HarnessAgentTurnState =
|
|
|
44
51
|
| 'awaiting-tool-result'
|
|
45
52
|
| 'suspended';
|
|
46
53
|
|
|
54
|
+
type ActivePromptControl = {
|
|
55
|
+
readonly turnId: number;
|
|
56
|
+
readonly promise: Promise<HarnessV1PromptControl | undefined>;
|
|
57
|
+
resolve(control: HarnessV1PromptControl | undefined): void;
|
|
58
|
+
settled: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
47
61
|
/**
|
|
48
62
|
* Live harness session held by the caller.
|
|
49
63
|
*
|
|
50
64
|
* Created by {@link import('./harness-agent').HarnessAgent.createSession}.
|
|
51
|
-
* Owns the underlying adapter session and
|
|
65
|
+
* Owns the underlying adapter session and holds its sandbox session.
|
|
52
66
|
*
|
|
53
67
|
* Pass the instance back to `agent.generate` / `agent.stream` on every
|
|
54
68
|
* call; end the local handle with `detach()`, `stop()`, or `destroy()`.
|
|
@@ -68,7 +82,10 @@ export class HarnessAgentSession {
|
|
|
68
82
|
private readonly sessionWorkDir: string;
|
|
69
83
|
private readonly ownsSandboxLifecycle: boolean;
|
|
70
84
|
private underlyingSession: HarnessAgentAdapterSession | undefined;
|
|
71
|
-
private sandboxSession:
|
|
85
|
+
private sandboxSession:
|
|
86
|
+
| HarnessV1NetworkSandboxSession
|
|
87
|
+
| SandboxSession
|
|
88
|
+
| undefined;
|
|
72
89
|
private readonly toolApproval:
|
|
73
90
|
| HarnessAgentToolApprovalConfiguration
|
|
74
91
|
| undefined;
|
|
@@ -84,6 +101,7 @@ export class HarnessAgentSession {
|
|
|
84
101
|
private turnState: HarnessAgentTurnState;
|
|
85
102
|
private turnSequence = 0;
|
|
86
103
|
private activeTurnSequence = 0;
|
|
104
|
+
private activePromptControl: ActivePromptControl | undefined;
|
|
87
105
|
private suspendedTurnState:
|
|
88
106
|
| Promise<HarnessAgentContinueTurnState>
|
|
89
107
|
| undefined;
|
|
@@ -98,7 +116,7 @@ export class HarnessAgentSession {
|
|
|
98
116
|
sessionId: string;
|
|
99
117
|
harness: HarnessAgentAdapter;
|
|
100
118
|
underlyingSession: HarnessAgentAdapterSession;
|
|
101
|
-
sandboxSession: HarnessV1NetworkSandboxSession;
|
|
119
|
+
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
102
120
|
ownsSandboxLifecycle?: boolean;
|
|
103
121
|
sessionWorkDir: string;
|
|
104
122
|
toolApproval: HarnessAgentToolApprovalConfiguration | undefined;
|
|
@@ -130,11 +148,11 @@ export class HarnessAgentSession {
|
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
/**
|
|
133
|
-
* Active
|
|
151
|
+
* Active sandbox session.
|
|
134
152
|
*
|
|
135
153
|
* @internal — accessed by session turn and lifecycle drivers.
|
|
136
154
|
*/
|
|
137
|
-
getSandboxSession(): HarnessV1NetworkSandboxSession {
|
|
155
|
+
getSandboxSession(): HarnessV1NetworkSandboxSession | SandboxSession {
|
|
138
156
|
if (this.sessionState !== 'active' || this.sandboxSession == null) {
|
|
139
157
|
throw new Error(
|
|
140
158
|
`Harness session ${this.sessionId} has ended and cannot be reused.`,
|
|
@@ -189,7 +207,7 @@ export class HarnessAgentSession {
|
|
|
189
207
|
activeTools: options.activeTools,
|
|
190
208
|
toolSpecs: options.toolSpecs,
|
|
191
209
|
builtinToolFiltering: options.builtinToolFiltering,
|
|
192
|
-
sandboxSession: sandboxSession
|
|
210
|
+
sandboxSession: getRestrictedSandboxSession(sandboxSession),
|
|
193
211
|
sessionWorkDir: this.sessionWorkDir,
|
|
194
212
|
runtimeContext: options.runtimeContext,
|
|
195
213
|
abortSignal: options.abortSignal,
|
|
@@ -220,6 +238,9 @@ export class HarnessAgentSession {
|
|
|
220
238
|
onTurnFailed: () => {
|
|
221
239
|
this.finishTrackedTurn({ turnId });
|
|
222
240
|
},
|
|
241
|
+
onPromptControlAvailable: control => {
|
|
242
|
+
this.setPromptControl({ turnId, control });
|
|
243
|
+
},
|
|
223
244
|
isTurnSuspending: () =>
|
|
224
245
|
this.activeTurnSequence === turnId && this.suspendedTurnState != null,
|
|
225
246
|
onStopConditionMet: () =>
|
|
@@ -269,7 +290,7 @@ export class HarnessAgentSession {
|
|
|
269
290
|
activeTools: options.activeTools,
|
|
270
291
|
toolSpecs: options.toolSpecs,
|
|
271
292
|
builtinToolFiltering: options.builtinToolFiltering,
|
|
272
|
-
sandboxSession: sandboxSession
|
|
293
|
+
sandboxSession: getRestrictedSandboxSession(sandboxSession),
|
|
273
294
|
sessionWorkDir: this.sessionWorkDir,
|
|
274
295
|
runtimeContext: options.runtimeContext,
|
|
275
296
|
abortSignal: options.abortSignal,
|
|
@@ -302,6 +323,9 @@ export class HarnessAgentSession {
|
|
|
302
323
|
onTurnFailed: () => {
|
|
303
324
|
this.finishTrackedTurn({ turnId });
|
|
304
325
|
},
|
|
326
|
+
onPromptControlAvailable: control => {
|
|
327
|
+
this.setPromptControl({ turnId, control });
|
|
328
|
+
},
|
|
305
329
|
isTurnSuspending: () =>
|
|
306
330
|
this.activeTurnSequence === turnId && this.suspendedTurnState != null,
|
|
307
331
|
onStopConditionMet: () =>
|
|
@@ -328,6 +352,49 @@ export class HarnessAgentSession {
|
|
|
328
352
|
await this.requireReusableSession().doCompact(customInstructions);
|
|
329
353
|
}
|
|
330
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Submit another user message to the active turn.
|
|
357
|
+
*
|
|
358
|
+
* The runtime accepts the message for its next safe input boundary. Output
|
|
359
|
+
* caused by the message remains part of the active turn's result stream.
|
|
360
|
+
*/
|
|
361
|
+
async experimental_steerTurn(text: string): Promise<void> {
|
|
362
|
+
this.requireReusableSession();
|
|
363
|
+
const activePromptControl = this.activePromptControl;
|
|
364
|
+
if (
|
|
365
|
+
this.turnState !== 'running' ||
|
|
366
|
+
this.suspendedTurnState != null ||
|
|
367
|
+
activePromptControl == null
|
|
368
|
+
) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`Harness session ${this.sessionId} has no running turn to steer.`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const control = await activePromptControl.promise;
|
|
375
|
+
if (
|
|
376
|
+
control == null ||
|
|
377
|
+
this.sessionState !== 'active' ||
|
|
378
|
+
this.turnState !== 'running' ||
|
|
379
|
+
this.suspendedTurnState != null ||
|
|
380
|
+
this.activePromptControl !== activePromptControl ||
|
|
381
|
+
this.activeTurnSequence !== activePromptControl.turnId
|
|
382
|
+
) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Harness session ${this.sessionId} no longer has the running turn targeted for steering.`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (control.submitUserMessage == null) {
|
|
389
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
390
|
+
message: `Harness '${this.harness.harnessId}' does not support steering active turns.`,
|
|
391
|
+
harnessId: this.harness.harnessId,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await control.submitUserMessage(text);
|
|
396
|
+
}
|
|
397
|
+
|
|
331
398
|
/**
|
|
332
399
|
* Park the session, returning a payload the caller can persist and later
|
|
333
400
|
* pass to `agent.createSession({ sessionId, resumeFrom })` to reconnect.
|
|
@@ -388,7 +455,7 @@ export class HarnessAgentSession {
|
|
|
388
455
|
return validated;
|
|
389
456
|
} finally {
|
|
390
457
|
this.endLocalHandle({ sessionState: 'stopped' });
|
|
391
|
-
if (this.ownsSandboxLifecycle) {
|
|
458
|
+
if (this.ownsSandboxLifecycle && 'stop' in sandboxSession) {
|
|
392
459
|
await Promise.resolve(sandboxSession.stop()).catch(() => {});
|
|
393
460
|
}
|
|
394
461
|
}
|
|
@@ -407,9 +474,11 @@ export class HarnessAgentSession {
|
|
|
407
474
|
await Promise.resolve(session.doDestroy()).catch(() => {});
|
|
408
475
|
}
|
|
409
476
|
if (!this.ownsSandboxLifecycle) return;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
477
|
+
if ('stop' in sandboxSession) {
|
|
478
|
+
await Promise.resolve(
|
|
479
|
+
sandboxSession.destroy?.() ?? sandboxSession.stop(),
|
|
480
|
+
).catch(() => {});
|
|
481
|
+
}
|
|
413
482
|
}
|
|
414
483
|
|
|
415
484
|
/**
|
|
@@ -474,6 +543,7 @@ export class HarnessAgentSession {
|
|
|
474
543
|
private async suspendCurrentTurn(options: {
|
|
475
544
|
session: HarnessAgentAdapterSession;
|
|
476
545
|
}): Promise<HarnessAgentContinueTurnState> {
|
|
546
|
+
this.clearActivePromptControl();
|
|
477
547
|
this.suspendedTurnState ??= (async () => {
|
|
478
548
|
const raw = await options.session.doSuspendTurn();
|
|
479
549
|
const validated = await validateLifecycleStateData({
|
|
@@ -546,12 +616,14 @@ export class HarnessAgentSession {
|
|
|
546
616
|
|
|
547
617
|
private markAwaitingApprovalIfActive(): void {
|
|
548
618
|
if (this.sessionState === 'active') {
|
|
619
|
+
this.clearActivePromptControl();
|
|
549
620
|
this.turnState = 'awaiting-approval';
|
|
550
621
|
}
|
|
551
622
|
}
|
|
552
623
|
|
|
553
624
|
private markAwaitingToolResultIfActive(): void {
|
|
554
625
|
if (this.sessionState === 'active') {
|
|
626
|
+
this.clearActivePromptControl();
|
|
555
627
|
this.turnState = 'awaiting-tool-result';
|
|
556
628
|
}
|
|
557
629
|
}
|
|
@@ -561,12 +633,63 @@ export class HarnessAgentSession {
|
|
|
561
633
|
this.activeTurnSequence = turnId;
|
|
562
634
|
this.suspendedTurnState = undefined;
|
|
563
635
|
this.turnState = 'running';
|
|
636
|
+
this.clearActivePromptControl();
|
|
637
|
+
let resolve!: (control: HarnessV1PromptControl | undefined) => void;
|
|
638
|
+
const promise = new Promise<HarnessV1PromptControl | undefined>(
|
|
639
|
+
resolvePromise => {
|
|
640
|
+
resolve = resolvePromise;
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
this.activePromptControl = {
|
|
644
|
+
turnId,
|
|
645
|
+
promise,
|
|
646
|
+
resolve,
|
|
647
|
+
settled: false,
|
|
648
|
+
};
|
|
564
649
|
return turnId;
|
|
565
650
|
}
|
|
566
651
|
|
|
652
|
+
private setPromptControl(options: {
|
|
653
|
+
turnId: number;
|
|
654
|
+
control: HarnessV1PromptControl;
|
|
655
|
+
}): void {
|
|
656
|
+
const activePromptControl = this.activePromptControl;
|
|
657
|
+
if (
|
|
658
|
+
this.sessionState !== 'active' ||
|
|
659
|
+
this.turnState !== 'running' ||
|
|
660
|
+
this.activeTurnSequence !== options.turnId ||
|
|
661
|
+
activePromptControl?.turnId !== options.turnId
|
|
662
|
+
) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
this.settleActivePromptControl(options.control);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private settleActivePromptControl(
|
|
669
|
+
control: HarnessV1PromptControl | undefined,
|
|
670
|
+
): void {
|
|
671
|
+
const activePromptControl = this.activePromptControl;
|
|
672
|
+
if (activePromptControl == null || activePromptControl.settled) return;
|
|
673
|
+
activePromptControl.settled = true;
|
|
674
|
+
activePromptControl.resolve(control);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private clearActivePromptControl(turnId?: number): void {
|
|
678
|
+
if (
|
|
679
|
+
turnId != null &&
|
|
680
|
+
this.activePromptControl != null &&
|
|
681
|
+
this.activePromptControl.turnId !== turnId
|
|
682
|
+
) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
this.settleActivePromptControl(undefined);
|
|
686
|
+
this.activePromptControl = undefined;
|
|
687
|
+
}
|
|
688
|
+
|
|
567
689
|
private finishTrackedTurn(options: { turnId: number }): void {
|
|
568
690
|
if (this.sessionState !== 'active') return;
|
|
569
691
|
if (this.activeTurnSequence !== options.turnId) return;
|
|
692
|
+
this.clearActivePromptControl(options.turnId);
|
|
570
693
|
this.pendingToolApprovals.clear();
|
|
571
694
|
this.pendingToolResults.clear();
|
|
572
695
|
this.suspendedTurnState = undefined;
|
|
@@ -576,6 +699,7 @@ export class HarnessAgentSession {
|
|
|
576
699
|
private endLocalHandle(options: {
|
|
577
700
|
sessionState: Exclude<HarnessAgentSessionState, 'active'>;
|
|
578
701
|
}): void {
|
|
702
|
+
this.clearActivePromptControl();
|
|
579
703
|
this.sessionState = options.sessionState;
|
|
580
704
|
this.underlyingSession = undefined;
|
|
581
705
|
this.sandboxSession = undefined;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
asSchema,
|
|
12
12
|
generateId,
|
|
13
13
|
type Context,
|
|
14
|
+
type Experimental_SandboxSession as SandboxSession,
|
|
14
15
|
type ModelMessage,
|
|
15
16
|
type ToolSet,
|
|
16
17
|
} from '@ai-sdk/provider-utils';
|
|
@@ -64,6 +65,8 @@ import {
|
|
|
64
65
|
resolvePermissionMode,
|
|
65
66
|
} from './internal/permission-mode';
|
|
66
67
|
import { resolveHarnessAgentToolFiltering } from './internal/tool-filtering';
|
|
68
|
+
import { resolveSandboxDefaultWorkingDirectory } from '../utils/resolve-sandbox-default-working-directory';
|
|
69
|
+
import { getRestrictedSandboxSession } from '../utils/get-restricted-sandbox-session';
|
|
67
70
|
|
|
68
71
|
export type { HarnessAllTools } from './harness-agent-tool-types';
|
|
69
72
|
|
|
@@ -108,12 +111,13 @@ export interface HarnessAgentCallExtensions {
|
|
|
108
111
|
* Adapter builtin tools (e.g. Claude Code's `Bash`) pass through
|
|
109
112
|
* untouched.
|
|
110
113
|
* - **Sandbox propagation.** On `createSession`, the agent uses a
|
|
111
|
-
* caller-provided network sandbox session when present; otherwise
|
|
112
|
-
* the configured provider's `createSession()` (or
|
|
113
|
-
* passes the selected session into `doStart`.
|
|
114
|
-
* `
|
|
115
|
-
* via `experimental_sandbox`. Caller-provided sandboxes
|
|
116
|
-
* caller and are not stopped or destroyed by the
|
|
114
|
+
* caller-provided network or basic sandbox session when present; otherwise
|
|
115
|
+
* it calls the configured provider's `createSession()` (or
|
|
116
|
+
* `resumeSession()`). It passes the selected session into `doStart`. A
|
|
117
|
+
* tool-safe `SandboxSession` is handed to user-tool
|
|
118
|
+
* `execute()` calls via `experimental_sandbox`. Caller-provided sandboxes
|
|
119
|
+
* remain owned by the caller and are not stopped or destroyed by the
|
|
120
|
+
* harness layer.
|
|
117
121
|
*/
|
|
118
122
|
export class HarnessAgent<
|
|
119
123
|
THarness extends HarnessAgentAdapter<any> = HarnessAgentAdapter,
|
|
@@ -234,10 +238,10 @@ export class HarnessAgent<
|
|
|
234
238
|
*/
|
|
235
239
|
continueFrom?: HarnessAgentContinueTurnState;
|
|
236
240
|
/**
|
|
237
|
-
* Existing
|
|
238
|
-
*
|
|
241
|
+
* Existing sandbox session to run the harness in. When provided, the
|
|
242
|
+
* caller retains ownership of the sandbox lifecycle.
|
|
239
243
|
*/
|
|
240
|
-
sandboxSession?: HarnessV1NetworkSandboxSession;
|
|
244
|
+
sandboxSession?: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
241
245
|
abortSignal?: AbortSignal;
|
|
242
246
|
}): Promise<HarnessAgentSession> {
|
|
243
247
|
const sessionId = options?.sessionId ?? generateId();
|
|
@@ -281,12 +285,19 @@ export class HarnessAgent<
|
|
|
281
285
|
// Acquires the concrete sandbox session, either by starting fresh and then
|
|
282
286
|
// creating a post-bootstrap snapshot, or by reusing a previously created
|
|
283
287
|
// snapshot based on the bootstrap-based hashes.
|
|
284
|
-
let sandboxSession: HarnessV1NetworkSandboxSession;
|
|
288
|
+
let sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
285
289
|
let sessionWorkDir: string;
|
|
286
290
|
if (providedSandboxSession != null) {
|
|
287
291
|
sandboxSession = providedSandboxSession;
|
|
292
|
+
const toolSafeSandboxSession =
|
|
293
|
+
getRestrictedSandboxSession(sandboxSession);
|
|
294
|
+
const defaultWorkingDirectory =
|
|
295
|
+
await resolveSandboxDefaultWorkingDirectory({
|
|
296
|
+
sandboxSession,
|
|
297
|
+
abortSignal,
|
|
298
|
+
});
|
|
288
299
|
sessionWorkDir = resolveSessionWorkDir({
|
|
289
|
-
defaultWorkingDirectory
|
|
300
|
+
defaultWorkingDirectory,
|
|
290
301
|
harnessId: harness.harnessId,
|
|
291
302
|
sessionId,
|
|
292
303
|
workDir: this.sandboxConfig.workDir,
|
|
@@ -297,10 +308,10 @@ export class HarnessAgent<
|
|
|
297
308
|
const recipeIdentity = await hashHarnessBootstrap(recipe);
|
|
298
309
|
try {
|
|
299
310
|
await applyBootstrapRecipe({
|
|
300
|
-
session:
|
|
311
|
+
session: toolSafeSandboxSession,
|
|
301
312
|
recipe,
|
|
302
313
|
identity: recipeIdentity,
|
|
303
|
-
defaultWorkingDirectory
|
|
314
|
+
defaultWorkingDirectory,
|
|
304
315
|
abortSignal,
|
|
305
316
|
});
|
|
306
317
|
} catch (err) {
|
|
@@ -325,12 +336,14 @@ export class HarnessAgent<
|
|
|
325
336
|
harnessId: harness.harnessId,
|
|
326
337
|
});
|
|
327
338
|
}
|
|
328
|
-
|
|
339
|
+
const resumedSandboxSession = await sandboxProvider.resumeSession({
|
|
329
340
|
sessionId,
|
|
330
341
|
abortSignal,
|
|
331
342
|
});
|
|
343
|
+
sandboxSession = resumedSandboxSession;
|
|
332
344
|
sessionWorkDir = resolveSessionWorkDir({
|
|
333
|
-
defaultWorkingDirectory:
|
|
345
|
+
defaultWorkingDirectory:
|
|
346
|
+
resumedSandboxSession.defaultWorkingDirectory,
|
|
334
347
|
harnessId: harness.harnessId,
|
|
335
348
|
sessionId,
|
|
336
349
|
workDir: this.sandboxConfig.workDir,
|
|
@@ -352,14 +365,16 @@ export class HarnessAgent<
|
|
|
352
365
|
settings: this.sandboxConfig,
|
|
353
366
|
});
|
|
354
367
|
|
|
355
|
-
|
|
368
|
+
const createdSandboxSession = await sandboxProvider.createSession({
|
|
356
369
|
sessionId,
|
|
357
370
|
abortSignal,
|
|
358
371
|
identity: sandboxBootstrapPlan.identity,
|
|
359
372
|
onFirstCreate: sandboxBootstrapPlan.onFirstCreate,
|
|
360
373
|
});
|
|
374
|
+
sandboxSession = createdSandboxSession;
|
|
361
375
|
sessionWorkDir = resolveSessionWorkDir({
|
|
362
|
-
defaultWorkingDirectory:
|
|
376
|
+
defaultWorkingDirectory:
|
|
377
|
+
createdSandboxSession.defaultWorkingDirectory,
|
|
363
378
|
harnessId: harness.harnessId,
|
|
364
379
|
sessionId,
|
|
365
380
|
workDir: sandboxBootstrapPlan.workDir,
|
|
@@ -375,10 +390,11 @@ export class HarnessAgent<
|
|
|
375
390
|
) {
|
|
376
391
|
try {
|
|
377
392
|
await applyBootstrapRecipe({
|
|
378
|
-
session:
|
|
393
|
+
session: createdSandboxSession.restricted(),
|
|
379
394
|
recipe: sandboxBootstrapPlan.recipe,
|
|
380
395
|
identity: sandboxBootstrapPlan.recipeIdentity,
|
|
381
|
-
defaultWorkingDirectory:
|
|
396
|
+
defaultWorkingDirectory:
|
|
397
|
+
createdSandboxSession.defaultWorkingDirectory,
|
|
382
398
|
abortSignal,
|
|
383
399
|
});
|
|
384
400
|
} catch (err) {
|
|
@@ -400,7 +416,7 @@ export class HarnessAgent<
|
|
|
400
416
|
});
|
|
401
417
|
if (this.sandboxConfig.onSession != null) {
|
|
402
418
|
await this.sandboxConfig.onSession({
|
|
403
|
-
session: sandboxSession
|
|
419
|
+
session: getRestrictedSandboxSession(sandboxSession),
|
|
404
420
|
sessionWorkDir,
|
|
405
421
|
abortSignal,
|
|
406
422
|
});
|
|
@@ -586,6 +602,20 @@ export class HarnessAgent<
|
|
|
586
602
|
return result;
|
|
587
603
|
}
|
|
588
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Submit another user message to a currently running session turn.
|
|
607
|
+
*
|
|
608
|
+
* The returned promise resolves after the runtime has accepted the message
|
|
609
|
+
* for its next safe input boundary. Output caused by the message remains in
|
|
610
|
+
* the current turn's stream.
|
|
611
|
+
*/
|
|
612
|
+
async experimental_steer(options: {
|
|
613
|
+
session: HarnessAgentSession;
|
|
614
|
+
text: string;
|
|
615
|
+
}): Promise<void> {
|
|
616
|
+
await options.session.experimental_steerTurn(options.text);
|
|
617
|
+
}
|
|
618
|
+
|
|
589
619
|
// ─── Internals ──────────────────────────────────────────────────────
|
|
590
620
|
|
|
591
621
|
private _startTurn(input: {
|
|
@@ -923,9 +953,11 @@ function resolveSandboxConfig(
|
|
|
923
953
|
}
|
|
924
954
|
|
|
925
955
|
async function cleanupAfterStartFailure(input: {
|
|
926
|
-
sandboxSession: HarnessV1NetworkSandboxSession;
|
|
956
|
+
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
927
957
|
ownsSandboxLifecycle: boolean;
|
|
928
958
|
}): Promise<void> {
|
|
929
959
|
if (!input.ownsSandboxLifecycle) return;
|
|
930
|
-
|
|
960
|
+
if ('stop' in input.sandboxSession) {
|
|
961
|
+
await Promise.resolve(input.sandboxSession.stop()).catch(() => {});
|
|
962
|
+
}
|
|
931
963
|
}
|
|
@@ -108,6 +108,7 @@ export function runPrompt<
|
|
|
108
108
|
onToolResultSettled?: (toolCallId: string) => void;
|
|
109
109
|
onTurnFinished?: () => void;
|
|
110
110
|
onTurnFailed?: () => void;
|
|
111
|
+
onPromptControlAvailable?: (control: HarnessV1PromptControl) => void;
|
|
111
112
|
/**
|
|
112
113
|
* Reports that the adapter stream closed because the host intentionally
|
|
113
114
|
* suspended the still-running turn at a workflow slice boundary.
|
|
@@ -213,6 +214,7 @@ export function runPrompt<
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
const { stream, control } = bridge;
|
|
217
|
+
input.onPromptControlAvailable?.(control);
|
|
216
218
|
const reader = stream.getReader();
|
|
217
219
|
const toolCallsByToolCallId = new Map<string, ToolCallTextStreamPart>();
|
|
218
220
|
const rawToolCallsByToolCallId = new Map<
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
3
3
|
import type { HarnessV1Bootstrap } from '../../v1';
|
|
4
|
+
import { resolveSandboxDefaultWorkingDirectory } from '../../utils/resolve-sandbox-default-working-directory';
|
|
4
5
|
import type { HarnessAgentSandboxConfig } from '../harness-agent-settings';
|
|
5
6
|
import { applyBootstrapRecipe, hashHarnessBootstrap } from './bootstrap-recipe';
|
|
6
7
|
|
|
@@ -148,8 +149,8 @@ export async function runSandboxBootstrap({
|
|
|
148
149
|
|
|
149
150
|
const resolvedDefaultWorkingDirectory =
|
|
150
151
|
defaultWorkingDirectory ??
|
|
151
|
-
(await
|
|
152
|
-
session,
|
|
152
|
+
(await resolveSandboxDefaultWorkingDirectory({
|
|
153
|
+
sandboxSession: session,
|
|
153
154
|
abortSignal,
|
|
154
155
|
}));
|
|
155
156
|
|
|
@@ -181,32 +182,6 @@ export async function runSandboxBootstrap({
|
|
|
181
182
|
await onBootstrap({ session, workDir: bootstrapWorkDir, abortSignal });
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
export async function resolveDefaultWorkingDirectory({
|
|
185
|
-
session,
|
|
186
|
-
abortSignal,
|
|
187
|
-
}: {
|
|
188
|
-
readonly session: SandboxSession;
|
|
189
|
-
readonly abortSignal?: AbortSignal;
|
|
190
|
-
}): Promise<string> {
|
|
191
|
-
const result = await session.run({
|
|
192
|
-
command: 'pwd',
|
|
193
|
-
abortSignal,
|
|
194
|
-
});
|
|
195
|
-
if (result.exitCode !== 0) {
|
|
196
|
-
throw new Error(
|
|
197
|
-
`Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const cwd = result.stdout.trim();
|
|
202
|
-
if (!posix.isAbsolute(cwd)) {
|
|
203
|
-
throw new Error(
|
|
204
|
-
`Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
return cwd === '/' ? cwd : cwd.replace(/\/+$/, '');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
185
|
export async function ensureSandboxDirectory({
|
|
211
186
|
session,
|
|
212
187
|
workDir,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
2
|
import type { HarnessAgentSandboxConfig } from './harness-agent-settings';
|
|
3
3
|
import type { HarnessAgentAdapter } from './harness-agent-types';
|
|
4
|
+
import { resolveSandboxDefaultWorkingDirectory } from '../utils/resolve-sandbox-default-working-directory';
|
|
4
5
|
import {
|
|
5
6
|
applyBootstrapRecipe,
|
|
6
7
|
hashHarnessBootstrap,
|
|
7
8
|
} from './internal/bootstrap-recipe';
|
|
8
9
|
import {
|
|
9
10
|
normalizeSandboxWorkDir,
|
|
10
|
-
resolveDefaultWorkingDirectory,
|
|
11
11
|
runSandboxBootstrap,
|
|
12
12
|
validateSandboxBootstrapSettings,
|
|
13
13
|
} from './internal/sandbox-bootstrap';
|
|
@@ -80,8 +80,8 @@ export async function prepareSandboxForHarness(options: {
|
|
|
80
80
|
|
|
81
81
|
const recipeIdentity = await hashHarnessBootstrap(recipe);
|
|
82
82
|
recipeIdentities[harness.harnessId] = recipeIdentity;
|
|
83
|
-
defaultWorkingDirectory ??= await
|
|
84
|
-
|
|
83
|
+
defaultWorkingDirectory ??= await resolveSandboxDefaultWorkingDirectory({
|
|
84
|
+
sandboxSession: options.session,
|
|
85
85
|
abortSignal: options.abortSignal,
|
|
86
86
|
});
|
|
87
87
|
await applyBootstrapRecipe({
|