@ai-sdk/harness 0.0.0 → 1.0.0-beta.14
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 +110 -0
- package/LICENSE +13 -0
- package/README.md +142 -0
- package/agent/index.ts +47 -0
- package/bridge/index.ts +10 -0
- package/dist/agent/index.d.ts +1521 -0
- package/dist/agent/index.js +2958 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/bridge/index.d.ts +111 -0
- package/dist/bridge/index.js +415 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/index.d.ts +1536 -0
- package/dist/index.js +15834 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.d.ts +225 -0
- package/dist/utils/index.js +12148 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +99 -1
- package/src/agent/harness-agent-session.ts +509 -0
- package/src/agent/harness-agent-settings.ts +131 -0
- package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
- package/src/agent/harness-agent-types.ts +50 -0
- package/src/agent/harness-agent.ts +819 -0
- package/src/agent/internal/bootstrap-recipe.ts +124 -0
- package/src/agent/internal/bridge-port-registry.ts +52 -0
- package/src/agent/internal/harness-stream-text-result.ts +720 -0
- package/src/agent/internal/lifecycle-state-validation.ts +95 -0
- package/src/agent/internal/permission-mode.ts +50 -0
- package/src/agent/internal/resolve-observability.ts +128 -0
- package/src/agent/internal/run-prompt.ts +813 -0
- package/src/agent/internal/strip-work-dir.ts +68 -0
- package/src/agent/internal/to-harness-stream.ts +75 -0
- package/src/agent/internal/translate-stream-part.ts +221 -0
- package/src/agent/internal/turn-telemetry.ts +359 -0
- package/src/agent/observability/file-reporter.ts +206 -0
- package/src/agent/observability/index.ts +15 -0
- package/src/agent/observability/trace-tree-reporter.ts +122 -0
- package/src/agent/observability/types.ts +86 -0
- package/src/agent/prewarm.ts +47 -0
- package/src/bridge/index.ts +702 -0
- package/src/errors/harness-capability-unsupported-error.ts +41 -0
- package/src/errors/harness-error.ts +22 -0
- package/src/index.ts +3 -0
- package/src/utils/bridge-ready.ts +277 -0
- package/src/utils/classify-disk-log.ts +43 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/sandbox-channel.ts +453 -0
- package/src/v1/harness-v1-bootstrap.ts +46 -0
- package/src/v1/harness-v1-bridge-protocol.ts +310 -0
- package/src/v1/harness-v1-builtin-tool.ts +138 -0
- package/src/v1/harness-v1-call-warning.ts +22 -0
- package/src/v1/harness-v1-diagnostic.ts +66 -0
- package/src/v1/harness-v1-lifecycle-state.ts +65 -0
- package/src/v1/harness-v1-metadata.ts +13 -0
- package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
- package/src/v1/harness-v1-observability.ts +20 -0
- package/src/v1/harness-v1-permission-mode.ts +11 -0
- package/src/v1/harness-v1-prompt-control.ts +41 -0
- package/src/v1/harness-v1-prompt.ts +11 -0
- package/src/v1/harness-v1-sandbox-provider.ts +76 -0
- package/src/v1/harness-v1-session.ts +272 -0
- package/src/v1/harness-v1-skill.ts +36 -0
- package/src/v1/harness-v1-stream-part.ts +363 -0
- package/src/v1/harness-v1-tool-spec.ts +31 -0
- package/src/v1/harness-v1.ts +83 -0
- package/src/v1/index.ts +93 -0
- package/utils/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1 +1,99 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai-sdk/harness",
|
|
3
|
+
"version": "1.0.0-beta.14",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"source": "./src/index.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*",
|
|
12
|
+
"src",
|
|
13
|
+
"agent",
|
|
14
|
+
"utils",
|
|
15
|
+
"bridge",
|
|
16
|
+
"!src/**/*.test.ts",
|
|
17
|
+
"!src/**/*.test-d.ts",
|
|
18
|
+
"!src/**/__snapshots__",
|
|
19
|
+
"!src/**/__fixtures__",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
"./package.json": "./package.json",
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./agent": {
|
|
31
|
+
"types": "./dist/agent/index.d.ts",
|
|
32
|
+
"import": "./dist/agent/index.js",
|
|
33
|
+
"default": "./dist/agent/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./utils": {
|
|
36
|
+
"types": "./dist/utils/index.d.ts",
|
|
37
|
+
"import": "./dist/utils/index.js",
|
|
38
|
+
"default": "./dist/utils/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./bridge": {
|
|
41
|
+
"types": "./dist/bridge/index.d.ts",
|
|
42
|
+
"import": "./dist/bridge/index.js",
|
|
43
|
+
"default": "./dist/bridge/index.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@ai-sdk/provider": "4.0.0-beta.19",
|
|
48
|
+
"@ai-sdk/provider-utils": "5.0.0-beta.49",
|
|
49
|
+
"ai": "7.0.0-beta.177"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"ws": "^8.20.1"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"ws": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "22.19.19",
|
|
61
|
+
"@types/ws": "^8.5.13",
|
|
62
|
+
"tsup": "^8.5.1",
|
|
63
|
+
"typescript": "5.8.3",
|
|
64
|
+
"ws": "^8.20.1",
|
|
65
|
+
"zod": "3.25.76",
|
|
66
|
+
"@vercel/ai-tsconfig": "0.0.0"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=22"
|
|
70
|
+
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public",
|
|
73
|
+
"provenance": true
|
|
74
|
+
},
|
|
75
|
+
"homepage": "https://ai-sdk.dev/docs",
|
|
76
|
+
"repository": {
|
|
77
|
+
"type": "git",
|
|
78
|
+
"url": "https://github.com/vercel/ai",
|
|
79
|
+
"directory": "packages/harness"
|
|
80
|
+
},
|
|
81
|
+
"bugs": {
|
|
82
|
+
"url": "https://github.com/vercel/ai/issues"
|
|
83
|
+
},
|
|
84
|
+
"keywords": [
|
|
85
|
+
"ai",
|
|
86
|
+
"harness",
|
|
87
|
+
"agent"
|
|
88
|
+
],
|
|
89
|
+
"scripts": {
|
|
90
|
+
"build": "pnpm clean && tsup --tsconfig tsconfig.build.json",
|
|
91
|
+
"build:watch": "pnpm clean && tsup --watch",
|
|
92
|
+
"clean": "del-cli dist *.tsbuildinfo",
|
|
93
|
+
"type-check": "tsc --build",
|
|
94
|
+
"test": "pnpm test:node && pnpm test:edge",
|
|
95
|
+
"test:watch": "vitest --config vitest.node.config.js",
|
|
96
|
+
"test:edge": "vitest --config vitest.edge.config.js --run",
|
|
97
|
+
"test:node": "vitest --config vitest.node.config.js --run"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import type { Context, ToolSet } from '@ai-sdk/provider-utils';
|
|
2
|
+
import type { StreamTextResult, TelemetryOptions } from 'ai';
|
|
3
|
+
import type { HarnessAgentToolApprovalConfiguration } from './harness-agent-settings';
|
|
4
|
+
import type {
|
|
5
|
+
HarnessV1NetworkSandboxSession,
|
|
6
|
+
HarnessV1SandboxProvider,
|
|
7
|
+
} from '../v1';
|
|
8
|
+
import type {
|
|
9
|
+
HarnessAgentAdapter,
|
|
10
|
+
HarnessAgentAdapterSession,
|
|
11
|
+
HarnessAgentContinueTurnState,
|
|
12
|
+
HarnessAgentPendingToolApproval,
|
|
13
|
+
HarnessAgentPrompt,
|
|
14
|
+
HarnessAgentResumeSessionState,
|
|
15
|
+
HarnessAgentToolSpec,
|
|
16
|
+
} from './harness-agent-types';
|
|
17
|
+
import type { HarnessAgentToolApprovalContinuation } from './harness-agent-tool-approval-continuation';
|
|
18
|
+
import { releaseBridgePort } from './internal/bridge-port-registry';
|
|
19
|
+
import { validateLifecycleStateData } from './internal/lifecycle-state-validation';
|
|
20
|
+
import { runPrompt } from './internal/run-prompt';
|
|
21
|
+
|
|
22
|
+
type HarnessAgentTurnResult<
|
|
23
|
+
TOOLS extends ToolSet,
|
|
24
|
+
RUNTIME_CONTEXT extends Context,
|
|
25
|
+
> = {
|
|
26
|
+
result: StreamTextResult<TOOLS, RUNTIME_CONTEXT, never>;
|
|
27
|
+
done: Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type HarnessAgentSessionState = 'active' | 'detached' | 'stopped' | 'destroyed';
|
|
31
|
+
|
|
32
|
+
type HarnessAgentTurnState =
|
|
33
|
+
| 'idle'
|
|
34
|
+
| 'running'
|
|
35
|
+
| 'awaiting-approval'
|
|
36
|
+
| 'suspended';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Live harness session held by the caller.
|
|
40
|
+
*
|
|
41
|
+
* Created by {@link import('./harness-agent').HarnessAgent.createSession}.
|
|
42
|
+
* Owns the underlying adapter session, the network sandbox session, and the
|
|
43
|
+
* bridge-port lease (when the provider wraps a caller-provided sandbox with a
|
|
44
|
+
* port pool).
|
|
45
|
+
*
|
|
46
|
+
* Pass the instance back to `agent.generate` / `agent.stream` on every
|
|
47
|
+
* call; end the local handle with `detach()`, `stop()`, or `destroy()`.
|
|
48
|
+
*
|
|
49
|
+
* After any lifecycle method has resolved, the session is unusable — any
|
|
50
|
+
* subsequent `generate`/`stream` call against it throws.
|
|
51
|
+
*/
|
|
52
|
+
export class HarnessAgentSession {
|
|
53
|
+
/**
|
|
54
|
+
* Stable identifier the harness adapter saw in `doStart`. The same
|
|
55
|
+
* string callers persist when they intend to resume the session in a
|
|
56
|
+
* future process.
|
|
57
|
+
*/
|
|
58
|
+
readonly sessionId: string;
|
|
59
|
+
|
|
60
|
+
private readonly harness: HarnessAgentAdapter;
|
|
61
|
+
private readonly sandboxProvider: HarnessV1SandboxProvider;
|
|
62
|
+
private readonly sessionWorkDir: string;
|
|
63
|
+
private underlyingSession: HarnessAgentAdapterSession | undefined;
|
|
64
|
+
private sandboxSession: HarnessV1NetworkSandboxSession | undefined;
|
|
65
|
+
private leasedBridgePort: number | undefined;
|
|
66
|
+
private readonly toolApproval:
|
|
67
|
+
| HarnessAgentToolApprovalConfiguration
|
|
68
|
+
| undefined;
|
|
69
|
+
private readonly pendingToolApprovals = new Map<
|
|
70
|
+
string,
|
|
71
|
+
HarnessAgentPendingToolApproval
|
|
72
|
+
>();
|
|
73
|
+
private sessionState: HarnessAgentSessionState = 'active';
|
|
74
|
+
private turnState: HarnessAgentTurnState;
|
|
75
|
+
private turnSequence = 0;
|
|
76
|
+
private activeTurnSequence = 0;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether this session was created from `resumeFrom` or `continueFrom`.
|
|
80
|
+
* Captured at construction so it survives lifecycle cleanup.
|
|
81
|
+
*/
|
|
82
|
+
readonly isResume: boolean;
|
|
83
|
+
|
|
84
|
+
constructor(options: {
|
|
85
|
+
sessionId: string;
|
|
86
|
+
harness: HarnessAgentAdapter;
|
|
87
|
+
underlyingSession: HarnessAgentAdapterSession;
|
|
88
|
+
sandboxSession: HarnessV1NetworkSandboxSession;
|
|
89
|
+
sandboxProvider: HarnessV1SandboxProvider;
|
|
90
|
+
leasedBridgePort?: number;
|
|
91
|
+
sessionWorkDir: string;
|
|
92
|
+
toolApproval: HarnessAgentToolApprovalConfiguration | undefined;
|
|
93
|
+
pendingToolApprovals?: readonly HarnessAgentPendingToolApproval[];
|
|
94
|
+
turnState?: HarnessAgentTurnState;
|
|
95
|
+
}) {
|
|
96
|
+
this.sessionId = options.sessionId;
|
|
97
|
+
this.harness = options.harness;
|
|
98
|
+
this.underlyingSession = options.underlyingSession;
|
|
99
|
+
this.sandboxSession = options.sandboxSession;
|
|
100
|
+
this.sandboxProvider = options.sandboxProvider;
|
|
101
|
+
this.leasedBridgePort = options.leasedBridgePort;
|
|
102
|
+
this.sessionWorkDir = options.sessionWorkDir;
|
|
103
|
+
this.toolApproval = options.toolApproval;
|
|
104
|
+
for (const approval of options.pendingToolApprovals ?? []) {
|
|
105
|
+
this.pendingToolApprovals.set(approval.approvalId, approval);
|
|
106
|
+
}
|
|
107
|
+
this.turnState =
|
|
108
|
+
options.turnState ??
|
|
109
|
+
(this.pendingToolApprovals.size > 0 ? 'awaiting-approval' : 'idle');
|
|
110
|
+
this.isResume = options.underlyingSession.isResume;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Active network sandbox session.
|
|
115
|
+
*
|
|
116
|
+
* @internal — accessed by session turn and lifecycle drivers.
|
|
117
|
+
*/
|
|
118
|
+
getSandboxSession(): HarnessV1NetworkSandboxSession {
|
|
119
|
+
if (this.sessionState !== 'active' || this.sandboxSession == null) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Harness session ${this.sessionId} has ended and cannot be reused.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return this.sandboxSession;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Working directory the agent runs in for this session. Used to strip the
|
|
129
|
+
* prefix from absolute paths in stream events before they reach consumers.
|
|
130
|
+
*
|
|
131
|
+
* @internal — accessed by session turn drivers.
|
|
132
|
+
*/
|
|
133
|
+
getSessionWorkDir(): string {
|
|
134
|
+
return this.sessionWorkDir;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
promptTurn<TOOLS extends ToolSet, RUNTIME_CONTEXT extends Context>(options: {
|
|
138
|
+
prompt: HarnessAgentPrompt;
|
|
139
|
+
instructions: string | undefined;
|
|
140
|
+
tools: TOOLS;
|
|
141
|
+
toolSpecs: HarnessAgentToolSpec[];
|
|
142
|
+
runtimeContext: RUNTIME_CONTEXT;
|
|
143
|
+
abortSignal: AbortSignal | undefined;
|
|
144
|
+
telemetry: TelemetryOptions | undefined;
|
|
145
|
+
}): HarnessAgentTurnResult<TOOLS, RUNTIME_CONTEXT> {
|
|
146
|
+
const session = this.requireReusableSession();
|
|
147
|
+
this.requirePromptableTurn();
|
|
148
|
+
const sandboxSession = this.getSandboxSession();
|
|
149
|
+
const turnId = this.startTrackedTurn();
|
|
150
|
+
try {
|
|
151
|
+
const turn = runPrompt<TOOLS, RUNTIME_CONTEXT>({
|
|
152
|
+
harness: this.harness,
|
|
153
|
+
session,
|
|
154
|
+
prompt: options.prompt,
|
|
155
|
+
instructions: options.instructions,
|
|
156
|
+
tools: options.tools,
|
|
157
|
+
toolSpecs: options.toolSpecs,
|
|
158
|
+
sandboxSession: sandboxSession.restricted(),
|
|
159
|
+
sessionWorkDir: this.sessionWorkDir,
|
|
160
|
+
runtimeContext: options.runtimeContext,
|
|
161
|
+
abortSignal: options.abortSignal,
|
|
162
|
+
telemetry: options.telemetry,
|
|
163
|
+
toolApproval: this.toolApproval,
|
|
164
|
+
pendingToolApprovals: this.getPendingToolApprovals(),
|
|
165
|
+
onPendingToolApproval: approval => {
|
|
166
|
+
this.pendingToolApprovals.set(approval.approvalId, approval);
|
|
167
|
+
this.markAwaitingApprovalIfActive();
|
|
168
|
+
},
|
|
169
|
+
onToolApprovalSettled: approvalId => {
|
|
170
|
+
this.pendingToolApprovals.delete(approvalId);
|
|
171
|
+
},
|
|
172
|
+
onTurnFinished: () => {
|
|
173
|
+
this.finishTrackedTurn({ turnId });
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
this.trackTurnCompletion({ done: turn.done, turnId });
|
|
177
|
+
return turn;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
this.finishTrackedTurn({ turnId });
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
continueTurn<
|
|
185
|
+
TOOLS extends ToolSet,
|
|
186
|
+
RUNTIME_CONTEXT extends Context,
|
|
187
|
+
>(options: {
|
|
188
|
+
instructions: string | undefined;
|
|
189
|
+
tools: TOOLS;
|
|
190
|
+
toolSpecs: HarnessAgentToolSpec[];
|
|
191
|
+
runtimeContext: RUNTIME_CONTEXT;
|
|
192
|
+
abortSignal: AbortSignal | undefined;
|
|
193
|
+
telemetry: TelemetryOptions | undefined;
|
|
194
|
+
toolApprovalContinuations?:
|
|
195
|
+
| readonly HarnessAgentToolApprovalContinuation[]
|
|
196
|
+
| undefined;
|
|
197
|
+
}): HarnessAgentTurnResult<TOOLS, RUNTIME_CONTEXT> {
|
|
198
|
+
const session = this.requireReusableSession();
|
|
199
|
+
this.requireContinuableTurn();
|
|
200
|
+
const sandboxSession = this.getSandboxSession();
|
|
201
|
+
const turnId = this.startTrackedTurn();
|
|
202
|
+
try {
|
|
203
|
+
const turn = runPrompt<TOOLS, RUNTIME_CONTEXT>({
|
|
204
|
+
harness: this.harness,
|
|
205
|
+
session,
|
|
206
|
+
mode: 'continue',
|
|
207
|
+
instructions: options.instructions,
|
|
208
|
+
tools: options.tools,
|
|
209
|
+
toolSpecs: options.toolSpecs,
|
|
210
|
+
sandboxSession: sandboxSession.restricted(),
|
|
211
|
+
sessionWorkDir: this.sessionWorkDir,
|
|
212
|
+
runtimeContext: options.runtimeContext,
|
|
213
|
+
abortSignal: options.abortSignal,
|
|
214
|
+
telemetry: options.telemetry,
|
|
215
|
+
toolApproval: this.toolApproval,
|
|
216
|
+
pendingToolApprovals: this.getPendingToolApprovals(),
|
|
217
|
+
toolApprovalContinuations: options.toolApprovalContinuations,
|
|
218
|
+
onPendingToolApproval: approval => {
|
|
219
|
+
this.pendingToolApprovals.set(approval.approvalId, approval);
|
|
220
|
+
this.markAwaitingApprovalIfActive();
|
|
221
|
+
},
|
|
222
|
+
onToolApprovalSettled: approvalId => {
|
|
223
|
+
this.pendingToolApprovals.delete(approvalId);
|
|
224
|
+
},
|
|
225
|
+
onTurnFinished: () => {
|
|
226
|
+
this.finishTrackedTurn({ turnId });
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
this.trackTurnCompletion({ done: turn.done, turnId });
|
|
230
|
+
return turn;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
this.finishTrackedTurn({ turnId });
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Ask the underlying runtime to compact its context. The runtime performs
|
|
239
|
+
* the compaction itself; when it completes, a `compaction` part appears on
|
|
240
|
+
* the active (or next) turn's stream. Safe to call between turns for
|
|
241
|
+
* runtimes whose compaction is session-scoped (e.g. Pi).
|
|
242
|
+
*
|
|
243
|
+
* Throws `HarnessCapabilityUnsupportedError` for harnesses that cannot
|
|
244
|
+
* trigger compaction manually (e.g. Codex, which still auto-compacts under
|
|
245
|
+
* the hood). Throws if the session has ended.
|
|
246
|
+
*/
|
|
247
|
+
async compact(customInstructions?: string): Promise<void> {
|
|
248
|
+
await this.requireReusableSession().doCompact(customInstructions);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Park the session, returning a payload the caller can persist and later
|
|
253
|
+
* pass to `agent.createSession({ sessionId, resumeFrom })` to reconnect.
|
|
254
|
+
* The runtime and sandbox keep running; this local session handle becomes
|
|
255
|
+
* unusable.
|
|
256
|
+
*/
|
|
257
|
+
async detach(): Promise<HarnessAgentResumeSessionState> {
|
|
258
|
+
if (this.sessionState !== 'active' || this.underlyingSession == null) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Harness session ${this.sessionId} is not active and cannot be detached.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const session = this.underlyingSession;
|
|
264
|
+
try {
|
|
265
|
+
if (this.turnState !== 'idle') {
|
|
266
|
+
return this.toResumeStateWithContinuation({
|
|
267
|
+
continueFrom: await this.suspendCurrentTurn({ session }),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const raw = await session.doDetach();
|
|
271
|
+
const validated = await validateLifecycleStateData({
|
|
272
|
+
harness: this.harness,
|
|
273
|
+
state: raw,
|
|
274
|
+
expectedType: 'resume-session',
|
|
275
|
+
});
|
|
276
|
+
return validated;
|
|
277
|
+
} finally {
|
|
278
|
+
this.endLocalHandle({
|
|
279
|
+
sessionState: 'detached',
|
|
280
|
+
releasePortLease: false,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Persist enough state to resume later, then stop the runtime and sandbox.
|
|
287
|
+
* Returns the resume state for a future
|
|
288
|
+
* `agent.createSession({ sessionId, resumeFrom })` call.
|
|
289
|
+
*/
|
|
290
|
+
async stop(): Promise<HarnessAgentResumeSessionState> {
|
|
291
|
+
if (this.sessionState !== 'active' || this.underlyingSession == null) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Harness session ${this.sessionId} is not active and cannot be stopped.`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const session = this.underlyingSession;
|
|
297
|
+
const sandboxSession = this.getSandboxSession();
|
|
298
|
+
try {
|
|
299
|
+
if (this.turnState !== 'idle') {
|
|
300
|
+
return this.toResumeStateWithContinuation({
|
|
301
|
+
continueFrom: await this.suspendCurrentTurn({ session }),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const raw = await session.doStop();
|
|
305
|
+
const validated = await validateLifecycleStateData({
|
|
306
|
+
harness: this.harness,
|
|
307
|
+
state: raw,
|
|
308
|
+
expectedType: 'resume-session',
|
|
309
|
+
});
|
|
310
|
+
return validated;
|
|
311
|
+
} finally {
|
|
312
|
+
this.endLocalHandle({
|
|
313
|
+
sessionState: 'stopped',
|
|
314
|
+
releasePortLease: true,
|
|
315
|
+
});
|
|
316
|
+
await Promise.resolve(sandboxSession.stop()).catch(() => {});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Stop the runtime and discard resumability. The sandbox is destroyed when
|
|
322
|
+
* the provider supports destruction; otherwise it is stopped.
|
|
323
|
+
*/
|
|
324
|
+
async destroy(): Promise<void> {
|
|
325
|
+
if (this.sessionState !== 'active') return;
|
|
326
|
+
const session = this.underlyingSession;
|
|
327
|
+
const sandboxSession = this.getSandboxSession();
|
|
328
|
+
this.endLocalHandle({ sessionState: 'destroyed', releasePortLease: true });
|
|
329
|
+
if (session != null) {
|
|
330
|
+
await Promise.resolve(session.doDestroy()).catch(() => {});
|
|
331
|
+
}
|
|
332
|
+
await Promise.resolve(
|
|
333
|
+
sandboxSession.destroy?.() ?? sandboxSession.stop(),
|
|
334
|
+
).catch(() => {});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Gracefully freeze the active turn at the slice boundary and return the
|
|
339
|
+
* continuation payload, **leaving the sandbox/runtime running** so the next
|
|
340
|
+
* process can continue. Resolves once the in-flight `stream()` /
|
|
341
|
+
* `continueStream()` has cleanly wound down at a precise cursor (see
|
|
342
|
+
* `doSuspendTurn`).
|
|
343
|
+
*
|
|
344
|
+
* After this call the session is detached. This in-process handle no
|
|
345
|
+
* longer drives turns; a future slice creates a fresh session from the
|
|
346
|
+
* returned state. The sandbox is **not** stopped and no port lease is
|
|
347
|
+
* released, because bridge-backed adapters may still have a live bridge on
|
|
348
|
+
* that port.
|
|
349
|
+
*/
|
|
350
|
+
async suspendTurn(): Promise<HarnessAgentContinueTurnState> {
|
|
351
|
+
if (this.sessionState !== 'active' || this.underlyingSession == null) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`Harness session ${this.sessionId} is not active and cannot be suspended.`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (this.turnState === 'idle') {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`Harness session ${this.sessionId} has no unfinished turn to suspend.`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const session = this.underlyingSession;
|
|
362
|
+
try {
|
|
363
|
+
return await this.suspendCurrentTurn({ session });
|
|
364
|
+
} finally {
|
|
365
|
+
this.endLocalHandle({
|
|
366
|
+
sessionState: 'detached',
|
|
367
|
+
releasePortLease: false,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private getPendingToolApprovals(): readonly HarnessAgentPendingToolApproval[] {
|
|
373
|
+
return Array.from(this.pendingToolApprovals.values());
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private addPendingToolApprovals(
|
|
377
|
+
state: HarnessAgentContinueTurnState,
|
|
378
|
+
): HarnessAgentContinueTurnState {
|
|
379
|
+
const pendingToolApprovals = this.getPendingToolApprovals();
|
|
380
|
+
if (pendingToolApprovals.length === 0) {
|
|
381
|
+
return {
|
|
382
|
+
type: state.type,
|
|
383
|
+
harnessId: state.harnessId,
|
|
384
|
+
specificationVersion: state.specificationVersion,
|
|
385
|
+
data: state.data,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
...state,
|
|
390
|
+
pendingToolApprovals,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private async suspendCurrentTurn(options: {
|
|
395
|
+
session: HarnessAgentAdapterSession;
|
|
396
|
+
}): Promise<HarnessAgentContinueTurnState> {
|
|
397
|
+
const raw = await options.session.doSuspendTurn();
|
|
398
|
+
const validated = await validateLifecycleStateData({
|
|
399
|
+
harness: this.harness,
|
|
400
|
+
state: raw,
|
|
401
|
+
expectedType: 'continue-turn',
|
|
402
|
+
});
|
|
403
|
+
this.turnState = 'suspended';
|
|
404
|
+
return this.addPendingToolApprovals(validated);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private toResumeStateWithContinuation(options: {
|
|
408
|
+
continueFrom: HarnessAgentContinueTurnState;
|
|
409
|
+
}): HarnessAgentResumeSessionState {
|
|
410
|
+
const { continueFrom } = options;
|
|
411
|
+
return {
|
|
412
|
+
type: 'resume-session',
|
|
413
|
+
harnessId: continueFrom.harnessId,
|
|
414
|
+
specificationVersion: continueFrom.specificationVersion,
|
|
415
|
+
data: continueFrom.data,
|
|
416
|
+
continueFrom,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private requirePromptableTurn(): void {
|
|
421
|
+
if (this.turnState === 'idle') return;
|
|
422
|
+
if (this.turnState === 'running') {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Harness session ${this.sessionId} already has a turn in progress.`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
throw new Error(
|
|
428
|
+
`Harness session ${this.sessionId} has an unfinished turn and must be continued before accepting a new prompt.`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private requireContinuableTurn(): void {
|
|
433
|
+
if (
|
|
434
|
+
this.turnState === 'awaiting-approval' ||
|
|
435
|
+
this.turnState === 'suspended'
|
|
436
|
+
) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (this.turnState === 'running') {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`Harness session ${this.sessionId} already has a turn in progress.`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
throw new Error(
|
|
445
|
+
`Harness session ${this.sessionId} has no unfinished turn to continue.`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private markAwaitingApprovalIfActive(): void {
|
|
450
|
+
if (this.sessionState === 'active') {
|
|
451
|
+
this.turnState = 'awaiting-approval';
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private startTrackedTurn(): number {
|
|
456
|
+
const turnId = ++this.turnSequence;
|
|
457
|
+
this.activeTurnSequence = turnId;
|
|
458
|
+
this.turnState = 'running';
|
|
459
|
+
return turnId;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private trackTurnCompletion(options: {
|
|
463
|
+
done: Promise<void>;
|
|
464
|
+
turnId: number;
|
|
465
|
+
}): void {
|
|
466
|
+
void Promise.resolve(options.done)
|
|
467
|
+
.finally(() => {
|
|
468
|
+
this.finishTrackedTurn({ turnId: options.turnId });
|
|
469
|
+
})
|
|
470
|
+
.catch(() => {});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private finishTrackedTurn(options: { turnId: number }): void {
|
|
474
|
+
if (this.sessionState !== 'active') return;
|
|
475
|
+
if (this.activeTurnSequence !== options.turnId) return;
|
|
476
|
+
this.turnState =
|
|
477
|
+
this.pendingToolApprovals.size > 0 ? 'awaiting-approval' : 'idle';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private endLocalHandle(options: {
|
|
481
|
+
sessionState: Exclude<HarnessAgentSessionState, 'active'>;
|
|
482
|
+
releasePortLease: boolean;
|
|
483
|
+
}): void {
|
|
484
|
+
this.sessionState = options.sessionState;
|
|
485
|
+
this.underlyingSession = undefined;
|
|
486
|
+
this.sandboxSession = undefined;
|
|
487
|
+
if (options.releasePortLease) {
|
|
488
|
+
this.releasePortLease();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private releasePortLease(): void {
|
|
493
|
+
if (this.leasedBridgePort == null) return;
|
|
494
|
+
releaseBridgePort({
|
|
495
|
+
poolKey: this.sandboxProvider,
|
|
496
|
+
sessionId: this.sessionId,
|
|
497
|
+
});
|
|
498
|
+
this.leasedBridgePort = undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private requireReusableSession(): HarnessAgentAdapterSession {
|
|
502
|
+
if (this.sessionState !== 'active' || this.underlyingSession == null) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
`Harness session ${this.sessionId} has ended and cannot be reused.`,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
return this.underlyingSession;
|
|
508
|
+
}
|
|
509
|
+
}
|