@ai-sdk/harness 0.0.0-6b196531-20260710185421
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 +414 -0
- package/LICENSE +13 -0
- package/README.md +176 -0
- package/agent/index.ts +56 -0
- package/bridge/index.ts +10 -0
- package/dist/agent/index.d.ts +1631 -0
- package/dist/agent/index.js +3491 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/bridge/index.d.ts +129 -0
- package/dist/bridge/index.js +482 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/index.d.ts +1587 -0
- package/dist/index.js +517 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.d.ts +329 -0
- package/dist/utils/index.js +1241 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +100 -0
- package/src/agent/harness-agent-session.ts +518 -0
- package/src/agent/harness-agent-settings.ts +187 -0
- package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
- package/src/agent/harness-agent-tool-types.ts +15 -0
- package/src/agent/harness-agent-types.ts +50 -0
- package/src/agent/harness-agent.ts +865 -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 +731 -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 +901 -0
- package/src/agent/internal/sandbox-bootstrap.ts +266 -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/tool-filtering.ts +114 -0
- package/src/agent/internal/translate-stream-part.ts +221 -0
- package/src/agent/internal/turn-telemetry.ts +361 -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/prepare-harness-sandbox-template.ts +68 -0
- package/src/agent/prepare-sandbox-for-harness.ts +165 -0
- package/src/bridge/index.ts +797 -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/ai-gateway-auth.ts +15 -0
- package/src/utils/bridge-diagnostics.ts +213 -0
- package/src/utils/bridge-ready.ts +277 -0
- package/src/utils/classify-disk-log.ts +43 -0
- package/src/utils/index.ts +31 -0
- package/src/utils/sandbox-channel.ts +525 -0
- package/src/utils/sandbox-home-dir.ts +22 -0
- package/src/utils/shell-quote.ts +3 -0
- package/src/utils/write-skills.ts +141 -0
- package/src/v1/harness-v1-bootstrap.ts +46 -0
- package/src/v1/harness-v1-bridge-protocol.ts +342 -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 +280 -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-filtering.ts +25 -0
- package/src/v1/harness-v1-tool-spec.ts +31 -0
- package/src/v1/harness-v1.ts +94 -0
- package/src/v1/index.ts +99 -0
- package/utils/index.ts +1 -0
|
@@ -0,0 +1,518 @@
|
|
|
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
|
+
HarnessV1BuiltinToolFiltering,
|
|
6
|
+
HarnessV1NetworkSandboxSession,
|
|
7
|
+
HarnessV1SandboxProvider,
|
|
8
|
+
} from '../v1';
|
|
9
|
+
import type {
|
|
10
|
+
HarnessAgentAdapter,
|
|
11
|
+
HarnessAgentAdapterSession,
|
|
12
|
+
HarnessAgentContinueTurnState,
|
|
13
|
+
HarnessAgentPendingToolApproval,
|
|
14
|
+
HarnessAgentPrompt,
|
|
15
|
+
HarnessAgentResumeSessionState,
|
|
16
|
+
HarnessAgentToolSpec,
|
|
17
|
+
} from './harness-agent-types';
|
|
18
|
+
import type { HarnessAgentToolApprovalContinuation } from './harness-agent-tool-approval-continuation';
|
|
19
|
+
import { releaseBridgePort } from './internal/bridge-port-registry';
|
|
20
|
+
import { validateLifecycleStateData } from './internal/lifecycle-state-validation';
|
|
21
|
+
import { runPrompt } from './internal/run-prompt';
|
|
22
|
+
|
|
23
|
+
type HarnessAgentTurnResult<
|
|
24
|
+
TOOLS extends ToolSet,
|
|
25
|
+
RUNTIME_CONTEXT extends Context,
|
|
26
|
+
> = {
|
|
27
|
+
result: StreamTextResult<TOOLS, RUNTIME_CONTEXT, never>;
|
|
28
|
+
done: Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type HarnessAgentSessionState = 'active' | 'detached' | 'stopped' | 'destroyed';
|
|
32
|
+
|
|
33
|
+
type HarnessAgentTurnState =
|
|
34
|
+
| 'idle'
|
|
35
|
+
| 'running'
|
|
36
|
+
| 'awaiting-approval'
|
|
37
|
+
| 'suspended';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Live harness session held by the caller.
|
|
41
|
+
*
|
|
42
|
+
* Created by {@link import('./harness-agent').HarnessAgent.createSession}.
|
|
43
|
+
* Owns the underlying adapter session, the network sandbox session, and the
|
|
44
|
+
* bridge-port lease (when the provider wraps a caller-provided sandbox with a
|
|
45
|
+
* port pool).
|
|
46
|
+
*
|
|
47
|
+
* Pass the instance back to `agent.generate` / `agent.stream` on every
|
|
48
|
+
* call; end the local handle with `detach()`, `stop()`, or `destroy()`.
|
|
49
|
+
*
|
|
50
|
+
* After any lifecycle method has resolved, the session is unusable — any
|
|
51
|
+
* subsequent `generate`/`stream` call against it throws.
|
|
52
|
+
*/
|
|
53
|
+
export class HarnessAgentSession {
|
|
54
|
+
/**
|
|
55
|
+
* Stable identifier the harness adapter saw in `doStart`. The same
|
|
56
|
+
* string callers persist when they intend to resume the session in a
|
|
57
|
+
* future process.
|
|
58
|
+
*/
|
|
59
|
+
readonly sessionId: string;
|
|
60
|
+
|
|
61
|
+
private readonly harness: HarnessAgentAdapter;
|
|
62
|
+
private readonly sandboxProvider: HarnessV1SandboxProvider;
|
|
63
|
+
private readonly sessionWorkDir: string;
|
|
64
|
+
private underlyingSession: HarnessAgentAdapterSession | undefined;
|
|
65
|
+
private sandboxSession: HarnessV1NetworkSandboxSession | undefined;
|
|
66
|
+
private leasedBridgePort: number | undefined;
|
|
67
|
+
private readonly toolApproval:
|
|
68
|
+
| HarnessAgentToolApprovalConfiguration
|
|
69
|
+
| undefined;
|
|
70
|
+
private readonly pendingToolApprovals = new Map<
|
|
71
|
+
string,
|
|
72
|
+
HarnessAgentPendingToolApproval
|
|
73
|
+
>();
|
|
74
|
+
private sessionState: HarnessAgentSessionState = 'active';
|
|
75
|
+
private turnState: HarnessAgentTurnState;
|
|
76
|
+
private turnSequence = 0;
|
|
77
|
+
private activeTurnSequence = 0;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether this session was created from `resumeFrom` or `continueFrom`.
|
|
81
|
+
* Captured at construction so it survives lifecycle cleanup.
|
|
82
|
+
*/
|
|
83
|
+
readonly isResume: boolean;
|
|
84
|
+
|
|
85
|
+
constructor(options: {
|
|
86
|
+
sessionId: string;
|
|
87
|
+
harness: HarnessAgentAdapter;
|
|
88
|
+
underlyingSession: HarnessAgentAdapterSession;
|
|
89
|
+
sandboxSession: HarnessV1NetworkSandboxSession;
|
|
90
|
+
sandboxProvider: HarnessV1SandboxProvider;
|
|
91
|
+
leasedBridgePort?: number;
|
|
92
|
+
sessionWorkDir: string;
|
|
93
|
+
toolApproval: HarnessAgentToolApprovalConfiguration | undefined;
|
|
94
|
+
pendingToolApprovals?: readonly HarnessAgentPendingToolApproval[];
|
|
95
|
+
turnState?: HarnessAgentTurnState;
|
|
96
|
+
}) {
|
|
97
|
+
this.sessionId = options.sessionId;
|
|
98
|
+
this.harness = options.harness;
|
|
99
|
+
this.underlyingSession = options.underlyingSession;
|
|
100
|
+
this.sandboxSession = options.sandboxSession;
|
|
101
|
+
this.sandboxProvider = options.sandboxProvider;
|
|
102
|
+
this.leasedBridgePort = options.leasedBridgePort;
|
|
103
|
+
this.sessionWorkDir = options.sessionWorkDir;
|
|
104
|
+
this.toolApproval = options.toolApproval;
|
|
105
|
+
for (const approval of options.pendingToolApprovals ?? []) {
|
|
106
|
+
this.pendingToolApprovals.set(approval.approvalId, approval);
|
|
107
|
+
}
|
|
108
|
+
this.turnState =
|
|
109
|
+
options.turnState ??
|
|
110
|
+
(this.pendingToolApprovals.size > 0 ? 'awaiting-approval' : 'idle');
|
|
111
|
+
this.isResume = options.underlyingSession.isResume;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Active network sandbox session.
|
|
116
|
+
*
|
|
117
|
+
* @internal — accessed by session turn and lifecycle drivers.
|
|
118
|
+
*/
|
|
119
|
+
getSandboxSession(): HarnessV1NetworkSandboxSession {
|
|
120
|
+
if (this.sessionState !== 'active' || this.sandboxSession == null) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Harness session ${this.sessionId} has ended and cannot be reused.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return this.sandboxSession;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Working directory the agent runs in for this session. Used to strip the
|
|
130
|
+
* prefix from absolute paths in stream events before they reach consumers.
|
|
131
|
+
*
|
|
132
|
+
* @internal — accessed by session turn drivers.
|
|
133
|
+
*/
|
|
134
|
+
getSessionWorkDir(): string {
|
|
135
|
+
return this.sessionWorkDir;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
promptTurn<TOOLS extends ToolSet, RUNTIME_CONTEXT extends Context>(options: {
|
|
139
|
+
prompt: HarnessAgentPrompt;
|
|
140
|
+
instructions: string | undefined;
|
|
141
|
+
tools: TOOLS;
|
|
142
|
+
activeTools: ToolSet;
|
|
143
|
+
toolSpecs: HarnessAgentToolSpec[];
|
|
144
|
+
builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
|
|
145
|
+
runtimeContext: RUNTIME_CONTEXT;
|
|
146
|
+
abortSignal: AbortSignal | undefined;
|
|
147
|
+
telemetry: TelemetryOptions | undefined;
|
|
148
|
+
}): HarnessAgentTurnResult<TOOLS, RUNTIME_CONTEXT> {
|
|
149
|
+
const session = this.requireReusableSession();
|
|
150
|
+
this.requirePromptableTurn();
|
|
151
|
+
const sandboxSession = this.getSandboxSession();
|
|
152
|
+
const turnId = this.startTrackedTurn();
|
|
153
|
+
try {
|
|
154
|
+
const turn = runPrompt<TOOLS, RUNTIME_CONTEXT>({
|
|
155
|
+
harness: this.harness,
|
|
156
|
+
session,
|
|
157
|
+
prompt: options.prompt,
|
|
158
|
+
instructions: options.instructions,
|
|
159
|
+
tools: options.tools,
|
|
160
|
+
activeTools: options.activeTools,
|
|
161
|
+
toolSpecs: options.toolSpecs,
|
|
162
|
+
builtinToolFiltering: options.builtinToolFiltering,
|
|
163
|
+
sandboxSession: sandboxSession.restricted(),
|
|
164
|
+
sessionWorkDir: this.sessionWorkDir,
|
|
165
|
+
runtimeContext: options.runtimeContext,
|
|
166
|
+
abortSignal: options.abortSignal,
|
|
167
|
+
telemetry: options.telemetry,
|
|
168
|
+
toolApproval: this.toolApproval,
|
|
169
|
+
pendingToolApprovals: this.getPendingToolApprovals(),
|
|
170
|
+
onPendingToolApproval: approval => {
|
|
171
|
+
this.pendingToolApprovals.set(approval.approvalId, approval);
|
|
172
|
+
this.markAwaitingApprovalIfActive();
|
|
173
|
+
},
|
|
174
|
+
onToolApprovalSettled: approvalId => {
|
|
175
|
+
this.pendingToolApprovals.delete(approvalId);
|
|
176
|
+
},
|
|
177
|
+
onTurnFinished: () => {
|
|
178
|
+
this.finishTrackedTurn({ turnId });
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
this.trackTurnCompletion({ done: turn.done, turnId });
|
|
182
|
+
return turn;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.finishTrackedTurn({ turnId });
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
continueTurn<
|
|
190
|
+
TOOLS extends ToolSet,
|
|
191
|
+
RUNTIME_CONTEXT extends Context,
|
|
192
|
+
>(options: {
|
|
193
|
+
instructions: string | undefined;
|
|
194
|
+
tools: TOOLS;
|
|
195
|
+
activeTools: ToolSet;
|
|
196
|
+
toolSpecs: HarnessAgentToolSpec[];
|
|
197
|
+
builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
|
|
198
|
+
runtimeContext: RUNTIME_CONTEXT;
|
|
199
|
+
abortSignal: AbortSignal | undefined;
|
|
200
|
+
telemetry: TelemetryOptions | undefined;
|
|
201
|
+
toolApprovalContinuations?:
|
|
202
|
+
| readonly HarnessAgentToolApprovalContinuation[]
|
|
203
|
+
| undefined;
|
|
204
|
+
}): HarnessAgentTurnResult<TOOLS, RUNTIME_CONTEXT> {
|
|
205
|
+
const session = this.requireReusableSession();
|
|
206
|
+
this.requireContinuableTurn();
|
|
207
|
+
const sandboxSession = this.getSandboxSession();
|
|
208
|
+
const turnId = this.startTrackedTurn();
|
|
209
|
+
try {
|
|
210
|
+
const turn = runPrompt<TOOLS, RUNTIME_CONTEXT>({
|
|
211
|
+
harness: this.harness,
|
|
212
|
+
session,
|
|
213
|
+
mode: 'continue',
|
|
214
|
+
instructions: options.instructions,
|
|
215
|
+
tools: options.tools,
|
|
216
|
+
activeTools: options.activeTools,
|
|
217
|
+
toolSpecs: options.toolSpecs,
|
|
218
|
+
builtinToolFiltering: options.builtinToolFiltering,
|
|
219
|
+
sandboxSession: sandboxSession.restricted(),
|
|
220
|
+
sessionWorkDir: this.sessionWorkDir,
|
|
221
|
+
runtimeContext: options.runtimeContext,
|
|
222
|
+
abortSignal: options.abortSignal,
|
|
223
|
+
telemetry: options.telemetry,
|
|
224
|
+
toolApproval: this.toolApproval,
|
|
225
|
+
pendingToolApprovals: this.getPendingToolApprovals(),
|
|
226
|
+
toolApprovalContinuations: options.toolApprovalContinuations,
|
|
227
|
+
onPendingToolApproval: approval => {
|
|
228
|
+
this.pendingToolApprovals.set(approval.approvalId, approval);
|
|
229
|
+
this.markAwaitingApprovalIfActive();
|
|
230
|
+
},
|
|
231
|
+
onToolApprovalSettled: approvalId => {
|
|
232
|
+
this.pendingToolApprovals.delete(approvalId);
|
|
233
|
+
},
|
|
234
|
+
onTurnFinished: () => {
|
|
235
|
+
this.finishTrackedTurn({ turnId });
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
this.trackTurnCompletion({ done: turn.done, turnId });
|
|
239
|
+
return turn;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
this.finishTrackedTurn({ turnId });
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Ask the underlying runtime to compact its context. The runtime performs
|
|
248
|
+
* the compaction itself; when it completes, a `compaction` part appears on
|
|
249
|
+
* the active (or next) turn's stream. Safe to call between turns for
|
|
250
|
+
* runtimes whose compaction is session-scoped (e.g. Pi).
|
|
251
|
+
*
|
|
252
|
+
* Throws `HarnessCapabilityUnsupportedError` for harnesses that cannot
|
|
253
|
+
* trigger compaction manually (e.g. Codex, which still auto-compacts under
|
|
254
|
+
* the hood). Throws if the session has ended.
|
|
255
|
+
*/
|
|
256
|
+
async compact(customInstructions?: string): Promise<void> {
|
|
257
|
+
await this.requireReusableSession().doCompact(customInstructions);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Park the session, returning a payload the caller can persist and later
|
|
262
|
+
* pass to `agent.createSession({ sessionId, resumeFrom })` to reconnect.
|
|
263
|
+
* The runtime and sandbox keep running; this local session handle becomes
|
|
264
|
+
* unusable.
|
|
265
|
+
*/
|
|
266
|
+
async detach(): Promise<HarnessAgentResumeSessionState> {
|
|
267
|
+
if (this.sessionState !== 'active' || this.underlyingSession == null) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Harness session ${this.sessionId} is not active and cannot be detached.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const session = this.underlyingSession;
|
|
273
|
+
try {
|
|
274
|
+
if (this.turnState !== 'idle') {
|
|
275
|
+
return this.toResumeStateWithContinuation({
|
|
276
|
+
continueFrom: await this.suspendCurrentTurn({ session }),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const raw = await session.doDetach();
|
|
280
|
+
const validated = await validateLifecycleStateData({
|
|
281
|
+
harness: this.harness,
|
|
282
|
+
state: raw,
|
|
283
|
+
expectedType: 'resume-session',
|
|
284
|
+
});
|
|
285
|
+
return validated;
|
|
286
|
+
} finally {
|
|
287
|
+
this.endLocalHandle({
|
|
288
|
+
sessionState: 'detached',
|
|
289
|
+
releasePortLease: false,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Persist enough state to resume later, then stop the runtime and sandbox.
|
|
296
|
+
* Returns the resume state for a future
|
|
297
|
+
* `agent.createSession({ sessionId, resumeFrom })` call.
|
|
298
|
+
*/
|
|
299
|
+
async stop(): Promise<HarnessAgentResumeSessionState> {
|
|
300
|
+
if (this.sessionState !== 'active' || this.underlyingSession == null) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Harness session ${this.sessionId} is not active and cannot be stopped.`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const session = this.underlyingSession;
|
|
306
|
+
const sandboxSession = this.getSandboxSession();
|
|
307
|
+
try {
|
|
308
|
+
if (this.turnState !== 'idle') {
|
|
309
|
+
return this.toResumeStateWithContinuation({
|
|
310
|
+
continueFrom: await this.suspendCurrentTurn({ session }),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const raw = await session.doStop();
|
|
314
|
+
const validated = await validateLifecycleStateData({
|
|
315
|
+
harness: this.harness,
|
|
316
|
+
state: raw,
|
|
317
|
+
expectedType: 'resume-session',
|
|
318
|
+
});
|
|
319
|
+
return validated;
|
|
320
|
+
} finally {
|
|
321
|
+
this.endLocalHandle({
|
|
322
|
+
sessionState: 'stopped',
|
|
323
|
+
releasePortLease: true,
|
|
324
|
+
});
|
|
325
|
+
await Promise.resolve(sandboxSession.stop()).catch(() => {});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Stop the runtime and discard resumability. The sandbox is destroyed when
|
|
331
|
+
* the provider supports destruction; otherwise it is stopped.
|
|
332
|
+
*/
|
|
333
|
+
async destroy(): Promise<void> {
|
|
334
|
+
if (this.sessionState !== 'active') return;
|
|
335
|
+
const session = this.underlyingSession;
|
|
336
|
+
const sandboxSession = this.getSandboxSession();
|
|
337
|
+
this.endLocalHandle({ sessionState: 'destroyed', releasePortLease: true });
|
|
338
|
+
if (session != null) {
|
|
339
|
+
await Promise.resolve(session.doDestroy()).catch(() => {});
|
|
340
|
+
}
|
|
341
|
+
await Promise.resolve(
|
|
342
|
+
sandboxSession.destroy?.() ?? sandboxSession.stop(),
|
|
343
|
+
).catch(() => {});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Gracefully freeze the active turn at the slice boundary and return the
|
|
348
|
+
* continuation payload, **leaving the sandbox/runtime running** so the next
|
|
349
|
+
* process can continue. Resolves once the in-flight `stream()` /
|
|
350
|
+
* `continueStream()` has cleanly wound down at a precise cursor (see
|
|
351
|
+
* `doSuspendTurn`).
|
|
352
|
+
*
|
|
353
|
+
* After this call the session is detached. This in-process handle no
|
|
354
|
+
* longer drives turns; a future slice creates a fresh session from the
|
|
355
|
+
* returned state. The sandbox is **not** stopped and no port lease is
|
|
356
|
+
* released, because bridge-backed adapters may still have a live bridge on
|
|
357
|
+
* that port.
|
|
358
|
+
*/
|
|
359
|
+
async suspendTurn(): Promise<HarnessAgentContinueTurnState> {
|
|
360
|
+
if (this.sessionState !== 'active' || this.underlyingSession == null) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Harness session ${this.sessionId} is not active and cannot be suspended.`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (this.turnState === 'idle') {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`Harness session ${this.sessionId} has no unfinished turn to suspend.`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const session = this.underlyingSession;
|
|
371
|
+
try {
|
|
372
|
+
return await this.suspendCurrentTurn({ session });
|
|
373
|
+
} finally {
|
|
374
|
+
this.endLocalHandle({
|
|
375
|
+
sessionState: 'detached',
|
|
376
|
+
releasePortLease: false,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private getPendingToolApprovals(): readonly HarnessAgentPendingToolApproval[] {
|
|
382
|
+
return Array.from(this.pendingToolApprovals.values());
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private addPendingToolApprovals(
|
|
386
|
+
state: HarnessAgentContinueTurnState,
|
|
387
|
+
): HarnessAgentContinueTurnState {
|
|
388
|
+
const pendingToolApprovals = this.getPendingToolApprovals();
|
|
389
|
+
if (pendingToolApprovals.length === 0) {
|
|
390
|
+
return {
|
|
391
|
+
type: state.type,
|
|
392
|
+
harnessId: state.harnessId,
|
|
393
|
+
specificationVersion: state.specificationVersion,
|
|
394
|
+
data: state.data,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
...state,
|
|
399
|
+
pendingToolApprovals,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private async suspendCurrentTurn(options: {
|
|
404
|
+
session: HarnessAgentAdapterSession;
|
|
405
|
+
}): Promise<HarnessAgentContinueTurnState> {
|
|
406
|
+
const raw = await options.session.doSuspendTurn();
|
|
407
|
+
const validated = await validateLifecycleStateData({
|
|
408
|
+
harness: this.harness,
|
|
409
|
+
state: raw,
|
|
410
|
+
expectedType: 'continue-turn',
|
|
411
|
+
});
|
|
412
|
+
this.turnState = 'suspended';
|
|
413
|
+
return this.addPendingToolApprovals(validated);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private toResumeStateWithContinuation(options: {
|
|
417
|
+
continueFrom: HarnessAgentContinueTurnState;
|
|
418
|
+
}): HarnessAgentResumeSessionState {
|
|
419
|
+
const { continueFrom } = options;
|
|
420
|
+
return {
|
|
421
|
+
type: 'resume-session',
|
|
422
|
+
harnessId: continueFrom.harnessId,
|
|
423
|
+
specificationVersion: continueFrom.specificationVersion,
|
|
424
|
+
data: continueFrom.data,
|
|
425
|
+
continueFrom,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private requirePromptableTurn(): void {
|
|
430
|
+
if (this.turnState === 'idle') return;
|
|
431
|
+
if (this.turnState === 'running') {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`Harness session ${this.sessionId} already has a turn in progress.`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
throw new Error(
|
|
437
|
+
`Harness session ${this.sessionId} has an unfinished turn and must be continued before accepting a new prompt.`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private requireContinuableTurn(): void {
|
|
442
|
+
if (
|
|
443
|
+
this.turnState === 'awaiting-approval' ||
|
|
444
|
+
this.turnState === 'suspended'
|
|
445
|
+
) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (this.turnState === 'running') {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Harness session ${this.sessionId} already has a turn in progress.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Harness session ${this.sessionId} has no unfinished turn to continue.`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private markAwaitingApprovalIfActive(): void {
|
|
459
|
+
if (this.sessionState === 'active') {
|
|
460
|
+
this.turnState = 'awaiting-approval';
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private startTrackedTurn(): number {
|
|
465
|
+
const turnId = ++this.turnSequence;
|
|
466
|
+
this.activeTurnSequence = turnId;
|
|
467
|
+
this.turnState = 'running';
|
|
468
|
+
return turnId;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private trackTurnCompletion(options: {
|
|
472
|
+
done: Promise<void>;
|
|
473
|
+
turnId: number;
|
|
474
|
+
}): void {
|
|
475
|
+
void Promise.resolve(options.done)
|
|
476
|
+
.finally(() => {
|
|
477
|
+
this.finishTrackedTurn({ turnId: options.turnId });
|
|
478
|
+
})
|
|
479
|
+
.catch(() => {});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private finishTrackedTurn(options: { turnId: number }): void {
|
|
483
|
+
if (this.sessionState !== 'active') return;
|
|
484
|
+
if (this.activeTurnSequence !== options.turnId) return;
|
|
485
|
+
this.turnState =
|
|
486
|
+
this.pendingToolApprovals.size > 0 ? 'awaiting-approval' : 'idle';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private endLocalHandle(options: {
|
|
490
|
+
sessionState: Exclude<HarnessAgentSessionState, 'active'>;
|
|
491
|
+
releasePortLease: boolean;
|
|
492
|
+
}): void {
|
|
493
|
+
this.sessionState = options.sessionState;
|
|
494
|
+
this.underlyingSession = undefined;
|
|
495
|
+
this.sandboxSession = undefined;
|
|
496
|
+
if (options.releasePortLease) {
|
|
497
|
+
this.releasePortLease();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private releasePortLease(): void {
|
|
502
|
+
if (this.leasedBridgePort == null) return;
|
|
503
|
+
releaseBridgePort({
|
|
504
|
+
poolKey: this.sandboxProvider,
|
|
505
|
+
sessionId: this.sessionId,
|
|
506
|
+
});
|
|
507
|
+
this.leasedBridgePort = undefined;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private requireReusableSession(): HarnessAgentAdapterSession {
|
|
511
|
+
if (this.sessionState !== 'active' || this.underlyingSession == null) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`Harness session ${this.sessionId} has ended and cannot be reused.`,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return this.underlyingSession;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessDebugConfig,
|
|
3
|
+
HarnessDiagnostic,
|
|
4
|
+
} from './observability/types';
|
|
5
|
+
import type { HarnessV1SandboxProvider } from '../v1';
|
|
6
|
+
import type {
|
|
7
|
+
HarnessAgentAdapter,
|
|
8
|
+
HarnessAgentPermissionMode,
|
|
9
|
+
HarnessAgentSkill,
|
|
10
|
+
} from './harness-agent-types';
|
|
11
|
+
import type {
|
|
12
|
+
Experimental_SandboxSession as SandboxSession,
|
|
13
|
+
ToolSet,
|
|
14
|
+
} from '@ai-sdk/provider-utils';
|
|
15
|
+
import type { ActiveTools, TelemetryOptions, ToolApprovalStatus } from 'ai';
|
|
16
|
+
import type { HarnessAllTools } from './harness-agent-tool-types';
|
|
17
|
+
|
|
18
|
+
export type HarnessAgentToolApprovalConfiguration = Readonly<
|
|
19
|
+
Record<string, ToolApprovalStatus>
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
export type HarnessAgentSandboxConfig = {
|
|
23
|
+
/**
|
|
24
|
+
* Optional fixed working directory for all sessions, relative to the
|
|
25
|
+
* sandbox's default working directory. When omitted, sessions keep the
|
|
26
|
+
* existing `<harnessId>-<sessionId>` work directory.
|
|
27
|
+
*/
|
|
28
|
+
readonly workDir?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Caller-controlled identity for `onBootstrap`. Change this whenever the
|
|
32
|
+
* bootstrap side effects should invalidate the reusable sandbox snapshot.
|
|
33
|
+
*/
|
|
34
|
+
readonly bootstrapHash?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Called during sandbox template creation after the harness adapter's own
|
|
38
|
+
* bootstrap has run and before snapshot-capable providers publish a snapshot.
|
|
39
|
+
*
|
|
40
|
+
* `bootstrapHash` must be provided with this callback.
|
|
41
|
+
*/
|
|
42
|
+
readonly onBootstrap?: (opts: {
|
|
43
|
+
readonly session: SandboxSession;
|
|
44
|
+
readonly workDir: string;
|
|
45
|
+
readonly abortSignal?: AbortSignal;
|
|
46
|
+
}) => Promise<void>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Called after each sandbox session is acquired and the session work
|
|
50
|
+
* directory exists, before the harness adapter starts. Runs for fresh and
|
|
51
|
+
* resumed sessions.
|
|
52
|
+
*
|
|
53
|
+
* Use this to write per-session config, install lightweight tools, activate
|
|
54
|
+
* licenses, or prepare files in `sessionWorkDir`. Keep it idempotent if the
|
|
55
|
+
* agent may resume sessions.
|
|
56
|
+
*/
|
|
57
|
+
readonly onSession?: (opts: {
|
|
58
|
+
readonly session: SandboxSession;
|
|
59
|
+
readonly sessionWorkDir: string;
|
|
60
|
+
readonly abortSignal?: AbortSignal;
|
|
61
|
+
}) => Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type HarnessTools<TOOLS extends ToolSet> = ActiveTools<NoInfer<TOOLS>>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Construction-time settings for a `HarnessAgent`.
|
|
68
|
+
*
|
|
69
|
+
* Per-call settings (prompt, abortSignal, callbacks) belong on the
|
|
70
|
+
* `AgentCallParameters` / `AgentStreamParameters` passed to `generate` /
|
|
71
|
+
* `stream` and are not duplicated here.
|
|
72
|
+
*/
|
|
73
|
+
type HarnessAgentToolFilteringSettings<TOOLS extends ToolSet> =
|
|
74
|
+
| {
|
|
75
|
+
/**
|
|
76
|
+
* Limits the tools that are available for the harness to call without
|
|
77
|
+
* changing the tool call and result types in the result.
|
|
78
|
+
*/
|
|
79
|
+
readonly activeTools?: HarnessTools<TOOLS>;
|
|
80
|
+
readonly inactiveTools?: never;
|
|
81
|
+
}
|
|
82
|
+
| {
|
|
83
|
+
readonly activeTools?: never;
|
|
84
|
+
/**
|
|
85
|
+
* Excludes tools from the set that is available for the harness to call
|
|
86
|
+
* without changing the tool call and result types in the result.
|
|
87
|
+
*/
|
|
88
|
+
readonly inactiveTools?: HarnessTools<TOOLS>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type HarnessAgentSettings<
|
|
92
|
+
THarness extends HarnessAgentAdapter<any> = HarnessAgentAdapter,
|
|
93
|
+
TUserTools extends ToolSet = {},
|
|
94
|
+
> = {
|
|
95
|
+
/**
|
|
96
|
+
* The harness adapter driving the underlying agent runtime. Its
|
|
97
|
+
* `builtinTools` are merged with the user-defined `tools` and exposed to
|
|
98
|
+
* AI SDK consumers in the typed `tool-call` stream.
|
|
99
|
+
*/
|
|
100
|
+
readonly harness: THarness;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stable identifier for this agent instance. Exposed via `agent.id`.
|
|
104
|
+
* If omitted, `agent.id` is `undefined`.
|
|
105
|
+
*/
|
|
106
|
+
readonly id?: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Tools available to the underlying runtime in addition to the harness's
|
|
110
|
+
* own builtins. The agent forwards each tool to the harness as a
|
|
111
|
+
* `HarnessAgentToolSpec`; when the runtime calls one, the agent executes
|
|
112
|
+
* `tool.execute()` on the host and submits the result back to the harness.
|
|
113
|
+
*
|
|
114
|
+
* User tools take precedence over harness builtins on key collision —
|
|
115
|
+
* declare a tool with the same name as a builtin to override.
|
|
116
|
+
*/
|
|
117
|
+
readonly tools?: TUserTools;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Skills made available to the underlying runtime for the lifetime of
|
|
121
|
+
* the session. Each adapter decides how to surface skills (file in the
|
|
122
|
+
* working tree, prompt prefix, …).
|
|
123
|
+
*/
|
|
124
|
+
readonly skills?: ReadonlyArray<HarnessAgentSkill>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Instructions for the underlying agent runtime. Adapters prepend this to
|
|
128
|
+
* the first user message of a fresh session, once — it is not re-applied on
|
|
129
|
+
* later turns or when resuming a previously ended session.
|
|
130
|
+
*/
|
|
131
|
+
readonly instructions?: string;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Built-in tool permission mode. Defaults to `'allow-all'`, preserving the
|
|
135
|
+
* existing bypass-permissions behavior unless users opt in.
|
|
136
|
+
*/
|
|
137
|
+
readonly permissionMode?: HarnessAgentPermissionMode;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Per custom-tool approval statuses. This mirrors AI SDK `toolApproval`
|
|
141
|
+
* object configuration for host-executed tools, without callback support.
|
|
142
|
+
*
|
|
143
|
+
* `not-applicable` and `approved` run the tool, `user-approval` pauses the
|
|
144
|
+
* turn for a user decision, and `denied` immediately submits an
|
|
145
|
+
* `execution-denied` result.
|
|
146
|
+
*/
|
|
147
|
+
readonly toolApproval?: HarnessAgentToolApprovalConfiguration;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sandbox provider whose `create()` produces the network sandbox session the
|
|
151
|
+
* harness runs against. Its `restricted()` view is also propagated to user
|
|
152
|
+
* tool `execute()` calls (as the `experimental_sandbox` field), typed as
|
|
153
|
+
* `Experimental_SandboxSession` so tools cannot reach the infra surface.
|
|
154
|
+
*/
|
|
155
|
+
readonly sandbox: HarnessV1SandboxProvider;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Sandbox working-directory and lifecycle hook configuration.
|
|
159
|
+
*/
|
|
160
|
+
readonly sandboxConfig?: HarnessAgentSandboxConfig;
|
|
161
|
+
|
|
162
|
+
/** @deprecated Use `sandboxConfig.onSession` instead. */
|
|
163
|
+
readonly onSandboxSession?: HarnessAgentSandboxConfig['onSession'];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Telemetry configuration. The harness drives AI SDK's pluggable
|
|
167
|
+
* `Telemetry` integration contract from the turn lifecycle, so a harness turn
|
|
168
|
+
* appears in a consumer's traces with the same span shape as `streamText`.
|
|
169
|
+
* Register an integration here (e.g. `@ai-sdk/otel`) or globally via
|
|
170
|
+
* `registerTelemetry`. The harness itself stays OpenTelemetry-agnostic.
|
|
171
|
+
*/
|
|
172
|
+
readonly telemetry?: TelemetryOptions;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Diagnostics configuration. Enables bridge log forwarding (sandbox
|
|
176
|
+
* console + structured `debug-event`s) and the `HARNESS_DEBUG` stderr default.
|
|
177
|
+
* Set `{ enabled: true }` to turn it on in code; env vars fill unset fields.
|
|
178
|
+
*/
|
|
179
|
+
readonly debug?: HarnessDebugConfig;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Programmatic sink for forwarded bridge diagnostics. Receives every
|
|
183
|
+
* captured console line and structured event, normalized. Independent of the
|
|
184
|
+
* stderr default — wire this to capture diagnostics in code.
|
|
185
|
+
*/
|
|
186
|
+
readonly onLog?: (event: HarnessDiagnostic) => void;
|
|
187
|
+
} & HarnessAgentToolFilteringSettings<HarnessAllTools<THarness, TUserTools>>;
|