@ai-sdk/harness 0.0.0 → 1.0.0-canary.3
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 +24 -0
- package/LICENSE +13 -0
- package/README.md +142 -0
- package/agent/index.ts +17 -0
- package/bridge/index.ts +10 -0
- package/dist/agent/index.d.ts +1480 -0
- package/dist/agent/index.js +2554 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/bridge/index.d.ts +111 -0
- package/dist/bridge/index.js +414 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/index.d.ts +1510 -0
- package/dist/index.js +15834 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/index.d.ts +97 -0
- package/dist/observability/index.js +225 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/utils/index.d.ts +196 -0
- package/dist/utils/index.js +327 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +104 -1
- package/src/agent/harness-agent-session.ts +352 -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.ts +750 -0
- package/src/agent/harness-diagnostics.ts +88 -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/permission-mode.ts +50 -0
- package/src/agent/internal/resolve-observability.ts +128 -0
- package/src/agent/internal/resume-state-validation.ts +51 -0
- package/src/agent/internal/run-prompt.ts +811 -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/prewarm.ts +46 -0
- package/src/bridge/index.ts +700 -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/observability/file-reporter.ts +209 -0
- package/src/observability/index.ts +13 -0
- package/src/observability/trace-tree-reporter.ts +122 -0
- package/src/utils/classify-disk-log.ts +43 -0
- package/src/utils/index.ts +7 -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-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-resume-state.ts +46 -0
- package/src/v1/harness-v1-sandbox-provider.ts +76 -0
- package/src/v1/harness-v1-session.ts +268 -0
- package/src/v1/harness-v1-skill.ts +22 -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
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessV1,
|
|
3
|
+
HarnessV1NetworkSandboxSession,
|
|
4
|
+
HarnessV1PendingToolApproval,
|
|
5
|
+
HarnessV1Prompt,
|
|
6
|
+
HarnessV1ResumeState,
|
|
7
|
+
HarnessV1SandboxProvider,
|
|
8
|
+
HarnessV1Session,
|
|
9
|
+
HarnessV1ToolSpec,
|
|
10
|
+
} from '../v1';
|
|
11
|
+
import type { Context, ToolSet } from '@ai-sdk/provider-utils';
|
|
12
|
+
import type { TelemetryOptions } from 'ai';
|
|
13
|
+
import type { HarnessAgentToolApprovalConfiguration } from './harness-agent-settings';
|
|
14
|
+
import type { HarnessAgentToolApprovalContinuation } from './harness-agent-tool-approval-continuation';
|
|
15
|
+
import { releaseBridgePort } from './internal/bridge-port-registry';
|
|
16
|
+
import { validateResumeStateData } from './internal/resume-state-validation';
|
|
17
|
+
import { runPrompt } from './internal/run-prompt';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Live harness session held by the caller.
|
|
21
|
+
*
|
|
22
|
+
* Created by {@link import('./harness-agent').HarnessAgent.createSession}.
|
|
23
|
+
* Owns the underlying `HarnessV1Session`, the network sandbox session, and the
|
|
24
|
+
* bridge-port lease (when the provider wraps a caller-provided sandbox with a
|
|
25
|
+
* port pool).
|
|
26
|
+
*
|
|
27
|
+
* Pass the instance back to `agent.generate` / `agent.stream` on every
|
|
28
|
+
* call; end the local handle with `detach()`, `stop()`, or `destroy()`.
|
|
29
|
+
*
|
|
30
|
+
* After any lifecycle method has resolved, the session is unusable — any
|
|
31
|
+
* subsequent `generate`/`stream` call against it throws.
|
|
32
|
+
*/
|
|
33
|
+
export class HarnessAgentSession {
|
|
34
|
+
/**
|
|
35
|
+
* Stable identifier the harness adapter saw in `doStart`. The same
|
|
36
|
+
* string callers persist when they intend to resume the session in a
|
|
37
|
+
* future process.
|
|
38
|
+
*/
|
|
39
|
+
readonly sessionId: string;
|
|
40
|
+
|
|
41
|
+
private readonly harness: HarnessV1;
|
|
42
|
+
private readonly sandboxProvider: HarnessV1SandboxProvider;
|
|
43
|
+
private readonly sessionWorkDir: string;
|
|
44
|
+
private underlyingSession: HarnessV1Session | undefined;
|
|
45
|
+
private sandboxSession: HarnessV1NetworkSandboxSession | undefined;
|
|
46
|
+
private leasedBridgePort: number | undefined;
|
|
47
|
+
private readonly toolApproval:
|
|
48
|
+
| HarnessAgentToolApprovalConfiguration
|
|
49
|
+
| undefined;
|
|
50
|
+
private readonly pendingToolApprovals = new Map<
|
|
51
|
+
string,
|
|
52
|
+
HarnessV1PendingToolApproval
|
|
53
|
+
>();
|
|
54
|
+
private stopped = false;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether this session was created from a resume payload. Captured at
|
|
58
|
+
* construction so it survives lifecycle cleanup.
|
|
59
|
+
*/
|
|
60
|
+
readonly isResume: boolean;
|
|
61
|
+
|
|
62
|
+
constructor(options: {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
harness: HarnessV1;
|
|
65
|
+
underlyingSession: HarnessV1Session;
|
|
66
|
+
sandboxSession: HarnessV1NetworkSandboxSession;
|
|
67
|
+
sandboxProvider: HarnessV1SandboxProvider;
|
|
68
|
+
leasedBridgePort?: number;
|
|
69
|
+
sessionWorkDir: string;
|
|
70
|
+
toolApproval: HarnessAgentToolApprovalConfiguration | undefined;
|
|
71
|
+
pendingToolApprovals?: readonly HarnessV1PendingToolApproval[];
|
|
72
|
+
}) {
|
|
73
|
+
this.sessionId = options.sessionId;
|
|
74
|
+
this.harness = options.harness;
|
|
75
|
+
this.underlyingSession = options.underlyingSession;
|
|
76
|
+
this.sandboxSession = options.sandboxSession;
|
|
77
|
+
this.sandboxProvider = options.sandboxProvider;
|
|
78
|
+
this.leasedBridgePort = options.leasedBridgePort;
|
|
79
|
+
this.sessionWorkDir = options.sessionWorkDir;
|
|
80
|
+
this.toolApproval = options.toolApproval;
|
|
81
|
+
for (const approval of options.pendingToolApprovals ?? []) {
|
|
82
|
+
this.pendingToolApprovals.set(approval.approvalId, approval);
|
|
83
|
+
}
|
|
84
|
+
this.isResume = options.underlyingSession.isResume;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Active network sandbox session.
|
|
89
|
+
*
|
|
90
|
+
* @internal — accessed by session turn and lifecycle drivers.
|
|
91
|
+
*/
|
|
92
|
+
getSandboxSession(): HarnessV1NetworkSandboxSession {
|
|
93
|
+
if (this.stopped || this.sandboxSession == null) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Harness session ${this.sessionId} has ended and cannot be reused.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return this.sandboxSession;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Working directory the agent runs in for this session. Used to strip the
|
|
103
|
+
* prefix from absolute paths in stream events before they reach consumers.
|
|
104
|
+
*
|
|
105
|
+
* @internal — accessed by session turn drivers.
|
|
106
|
+
*/
|
|
107
|
+
getSessionWorkDir(): string {
|
|
108
|
+
return this.sessionWorkDir;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
promptTurn<TOOLS extends ToolSet, RUNTIME_CONTEXT extends Context>(options: {
|
|
112
|
+
prompt: HarnessV1Prompt;
|
|
113
|
+
instructions: string | undefined;
|
|
114
|
+
tools: TOOLS;
|
|
115
|
+
toolSpecs: HarnessV1ToolSpec[];
|
|
116
|
+
runtimeContext: RUNTIME_CONTEXT;
|
|
117
|
+
abortSignal: AbortSignal | undefined;
|
|
118
|
+
telemetry: TelemetryOptions | undefined;
|
|
119
|
+
}): ReturnType<typeof runPrompt<TOOLS, RUNTIME_CONTEXT>> {
|
|
120
|
+
const session = this.requireReusableSession();
|
|
121
|
+
if (this.pendingToolApprovals.size > 0) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Harness session ${this.sessionId} has pending tool approvals and must be continued with approval responses before accepting a new prompt.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const sandboxSession = this.getSandboxSession();
|
|
127
|
+
return runPrompt<TOOLS, RUNTIME_CONTEXT>({
|
|
128
|
+
harness: this.harness,
|
|
129
|
+
session,
|
|
130
|
+
prompt: options.prompt,
|
|
131
|
+
instructions: options.instructions,
|
|
132
|
+
tools: options.tools,
|
|
133
|
+
toolSpecs: options.toolSpecs,
|
|
134
|
+
sandboxSession: sandboxSession.restricted(),
|
|
135
|
+
sessionWorkDir: this.sessionWorkDir,
|
|
136
|
+
runtimeContext: options.runtimeContext,
|
|
137
|
+
abortSignal: options.abortSignal,
|
|
138
|
+
telemetry: options.telemetry,
|
|
139
|
+
toolApproval: this.toolApproval,
|
|
140
|
+
pendingToolApprovals: this.getPendingToolApprovals(),
|
|
141
|
+
onPendingToolApproval: approval => {
|
|
142
|
+
this.pendingToolApprovals.set(approval.approvalId, approval);
|
|
143
|
+
},
|
|
144
|
+
onToolApprovalSettled: approvalId => {
|
|
145
|
+
this.pendingToolApprovals.delete(approvalId);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
continueTurn<
|
|
151
|
+
TOOLS extends ToolSet,
|
|
152
|
+
RUNTIME_CONTEXT extends Context,
|
|
153
|
+
>(options: {
|
|
154
|
+
instructions: string | undefined;
|
|
155
|
+
tools: TOOLS;
|
|
156
|
+
toolSpecs: HarnessV1ToolSpec[];
|
|
157
|
+
runtimeContext: RUNTIME_CONTEXT;
|
|
158
|
+
abortSignal: AbortSignal | undefined;
|
|
159
|
+
telemetry: TelemetryOptions | undefined;
|
|
160
|
+
toolApprovalContinuations?:
|
|
161
|
+
| readonly HarnessAgentToolApprovalContinuation[]
|
|
162
|
+
| undefined;
|
|
163
|
+
}): ReturnType<typeof runPrompt<TOOLS, RUNTIME_CONTEXT>> {
|
|
164
|
+
const session = this.requireReusableSession();
|
|
165
|
+
const sandboxSession = this.getSandboxSession();
|
|
166
|
+
return runPrompt<TOOLS, RUNTIME_CONTEXT>({
|
|
167
|
+
harness: this.harness,
|
|
168
|
+
session,
|
|
169
|
+
mode: 'continue',
|
|
170
|
+
instructions: options.instructions,
|
|
171
|
+
tools: options.tools,
|
|
172
|
+
toolSpecs: options.toolSpecs,
|
|
173
|
+
sandboxSession: sandboxSession.restricted(),
|
|
174
|
+
sessionWorkDir: this.sessionWorkDir,
|
|
175
|
+
runtimeContext: options.runtimeContext,
|
|
176
|
+
abortSignal: options.abortSignal,
|
|
177
|
+
telemetry: options.telemetry,
|
|
178
|
+
toolApproval: this.toolApproval,
|
|
179
|
+
pendingToolApprovals: this.getPendingToolApprovals(),
|
|
180
|
+
toolApprovalContinuations: options.toolApprovalContinuations,
|
|
181
|
+
onPendingToolApproval: approval => {
|
|
182
|
+
this.pendingToolApprovals.set(approval.approvalId, approval);
|
|
183
|
+
},
|
|
184
|
+
onToolApprovalSettled: approvalId => {
|
|
185
|
+
this.pendingToolApprovals.delete(approvalId);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Ask the underlying runtime to compact its context. The runtime performs
|
|
192
|
+
* the compaction itself; when it completes, a `compaction` part appears on
|
|
193
|
+
* the active (or next) turn's stream. Safe to call between turns for
|
|
194
|
+
* runtimes whose compaction is session-scoped (e.g. Pi).
|
|
195
|
+
*
|
|
196
|
+
* Throws `HarnessCapabilityUnsupportedError` for harnesses that cannot
|
|
197
|
+
* trigger compaction manually (e.g. Codex, which still auto-compacts under
|
|
198
|
+
* the hood). Throws if the session has ended.
|
|
199
|
+
*/
|
|
200
|
+
async compact(customInstructions?: string): Promise<void> {
|
|
201
|
+
await this.requireReusableSession().doCompact(customInstructions);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Park the session, returning a payload the caller can persist and later
|
|
206
|
+
* pass to `agent.createSession({ sessionId, resumeFrom })` to reconnect.
|
|
207
|
+
* The runtime and sandbox keep running; this local session handle becomes
|
|
208
|
+
* unusable.
|
|
209
|
+
*/
|
|
210
|
+
async detach(): Promise<HarnessV1ResumeState> {
|
|
211
|
+
if (this.stopped || this.underlyingSession == null) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Harness session ${this.sessionId} is not active and cannot be detached.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const session = this.underlyingSession;
|
|
217
|
+
try {
|
|
218
|
+
const raw = await session.doDetach();
|
|
219
|
+
const validated = await validateResumeStateData({
|
|
220
|
+
harness: this.harness,
|
|
221
|
+
state: raw as HarnessV1ResumeState,
|
|
222
|
+
});
|
|
223
|
+
return this.withPendingToolApprovals(validated);
|
|
224
|
+
} finally {
|
|
225
|
+
this.endLocalHandle({ releasePortLease: false });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Persist enough state to resume later, then stop the runtime and sandbox.
|
|
231
|
+
* Returns the resume state for a future
|
|
232
|
+
* `agent.createSession({ sessionId, resumeFrom })` call.
|
|
233
|
+
*/
|
|
234
|
+
async stop(): Promise<HarnessV1ResumeState> {
|
|
235
|
+
if (this.stopped || this.underlyingSession == null) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Harness session ${this.sessionId} is not active and cannot be stopped.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const session = this.underlyingSession;
|
|
241
|
+
const sandboxSession = this.getSandboxSession();
|
|
242
|
+
try {
|
|
243
|
+
const raw = await session.doStop();
|
|
244
|
+
const validated = await validateResumeStateData({
|
|
245
|
+
harness: this.harness,
|
|
246
|
+
state: raw as HarnessV1ResumeState,
|
|
247
|
+
});
|
|
248
|
+
return this.withPendingToolApprovals(validated);
|
|
249
|
+
} finally {
|
|
250
|
+
this.endLocalHandle({ releasePortLease: true });
|
|
251
|
+
await Promise.resolve(sandboxSession.stop()).catch(() => {});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Stop the runtime and discard resumability. The sandbox is destroyed when
|
|
257
|
+
* the provider supports destruction; otherwise it is stopped.
|
|
258
|
+
*/
|
|
259
|
+
async destroy(): Promise<void> {
|
|
260
|
+
if (this.stopped) return;
|
|
261
|
+
const session = this.underlyingSession;
|
|
262
|
+
const sandboxSession = this.getSandboxSession();
|
|
263
|
+
this.endLocalHandle({ releasePortLease: true });
|
|
264
|
+
if (session != null) {
|
|
265
|
+
await Promise.resolve(session.doDestroy()).catch(() => {});
|
|
266
|
+
}
|
|
267
|
+
await Promise.resolve(
|
|
268
|
+
sandboxSession.destroy?.() ?? sandboxSession.stop(),
|
|
269
|
+
).catch(() => {});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Gracefully freeze the active turn at the slice boundary and return the
|
|
274
|
+
* resume payload, **leaving the sandbox/runtime running** so the next process
|
|
275
|
+
* can resume. Resolves once the in-flight `stream()`/`continueTurn()`
|
|
276
|
+
* has cleanly wound down at a precise cursor (see
|
|
277
|
+
* {@link HarnessV1Session.doSuspendTurn}).
|
|
278
|
+
*
|
|
279
|
+
* After this call the session is marked stopped. This in-process handle no
|
|
280
|
+
* longer drives turns; a future slice creates a fresh session from the
|
|
281
|
+
* returned state. The sandbox is **not** stopped and no port lease is
|
|
282
|
+
* released, because bridge-backed adapters may still have a live bridge on
|
|
283
|
+
* that port.
|
|
284
|
+
*/
|
|
285
|
+
async suspendTurn(): Promise<HarnessV1ResumeState> {
|
|
286
|
+
if (this.stopped || this.underlyingSession == null) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Harness session ${this.sessionId} is not active and cannot be suspended.`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const session = this.underlyingSession;
|
|
292
|
+
const raw = await session.doSuspendTurn();
|
|
293
|
+
const validated = await validateResumeStateData({
|
|
294
|
+
harness: this.harness,
|
|
295
|
+
state: raw as HarnessV1ResumeState,
|
|
296
|
+
});
|
|
297
|
+
// Drop the in-process references without stopping the sandbox or releasing
|
|
298
|
+
// the port lease: bridge-backed adapters may keep using that port.
|
|
299
|
+
this.stopped = true;
|
|
300
|
+
this.underlyingSession = undefined;
|
|
301
|
+
this.sandboxSession = undefined;
|
|
302
|
+
return this.withPendingToolApprovals(validated);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private getPendingToolApprovals(): readonly HarnessV1PendingToolApproval[] {
|
|
306
|
+
return Array.from(this.pendingToolApprovals.values());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private withPendingToolApprovals(
|
|
310
|
+
state: HarnessV1ResumeState,
|
|
311
|
+
): HarnessV1ResumeState {
|
|
312
|
+
const pendingToolApprovals = this.getPendingToolApprovals();
|
|
313
|
+
if (pendingToolApprovals.length === 0) {
|
|
314
|
+
return {
|
|
315
|
+
harnessId: state.harnessId,
|
|
316
|
+
specificationVersion: state.specificationVersion,
|
|
317
|
+
data: state.data,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
...state,
|
|
322
|
+
pendingToolApprovals,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private endLocalHandle(options: { releasePortLease: boolean }): void {
|
|
327
|
+
this.stopped = true;
|
|
328
|
+
this.underlyingSession = undefined;
|
|
329
|
+
this.sandboxSession = undefined;
|
|
330
|
+
if (options.releasePortLease) {
|
|
331
|
+
this.releasePortLease();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private releasePortLease(): void {
|
|
336
|
+
if (this.leasedBridgePort == null) return;
|
|
337
|
+
releaseBridgePort({
|
|
338
|
+
poolKey: this.sandboxProvider,
|
|
339
|
+
sessionId: this.sessionId,
|
|
340
|
+
});
|
|
341
|
+
this.leasedBridgePort = undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private requireReusableSession(): HarnessV1Session {
|
|
345
|
+
if (this.stopped || this.underlyingSession == null) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`Harness session ${this.sessionId} has ended and cannot be reused.`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return this.underlyingSession;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessV1,
|
|
3
|
+
HarnessV1PermissionMode,
|
|
4
|
+
HarnessV1SandboxProvider,
|
|
5
|
+
HarnessV1Skill,
|
|
6
|
+
} from '../v1';
|
|
7
|
+
import type {
|
|
8
|
+
HarnessDebugConfig,
|
|
9
|
+
HarnessDiagnostic,
|
|
10
|
+
} from './harness-diagnostics';
|
|
11
|
+
import type {
|
|
12
|
+
Experimental_SandboxSession as SandboxSession,
|
|
13
|
+
ToolSet,
|
|
14
|
+
} from '@ai-sdk/provider-utils';
|
|
15
|
+
import type { TelemetryOptions, ToolApprovalStatus } from 'ai';
|
|
16
|
+
|
|
17
|
+
export type HarnessAgentToolApprovalConfiguration = Readonly<
|
|
18
|
+
Record<string, ToolApprovalStatus>
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Construction-time settings for a `HarnessAgent`.
|
|
23
|
+
*
|
|
24
|
+
* Per-call settings (prompt, abortSignal, callbacks) belong on the
|
|
25
|
+
* `AgentCallParameters` / `AgentStreamParameters` passed to `generate` /
|
|
26
|
+
* `stream` and are not duplicated here.
|
|
27
|
+
*/
|
|
28
|
+
export type HarnessAgentSettings<
|
|
29
|
+
THarness extends HarnessV1<any> = HarnessV1,
|
|
30
|
+
TUserTools extends ToolSet = {},
|
|
31
|
+
> = {
|
|
32
|
+
/**
|
|
33
|
+
* The harness adapter driving the underlying agent runtime. Its
|
|
34
|
+
* `builtinTools` are merged with the user-defined `tools` and exposed to
|
|
35
|
+
* AI SDK consumers in the typed `tool-call` stream.
|
|
36
|
+
*/
|
|
37
|
+
readonly harness: THarness;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Stable identifier for this agent instance. Exposed via `agent.id`.
|
|
41
|
+
* If omitted, `agent.id` is `undefined`.
|
|
42
|
+
*/
|
|
43
|
+
readonly id?: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Tools available to the underlying runtime in addition to the harness's
|
|
47
|
+
* own builtins. The agent forwards each tool to the harness as a
|
|
48
|
+
* `HarnessV1ToolSpec`; when the runtime calls one, the agent executes
|
|
49
|
+
* `tool.execute()` on the host and submits the result back to the harness.
|
|
50
|
+
*
|
|
51
|
+
* User tools take precedence over harness builtins on key collision —
|
|
52
|
+
* declare a tool with the same name as a builtin to override.
|
|
53
|
+
*/
|
|
54
|
+
readonly tools?: TUserTools;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Skills made available to the underlying runtime for the lifetime of
|
|
58
|
+
* the session. Each adapter decides how to surface skills (file in the
|
|
59
|
+
* working tree, prompt prefix, …).
|
|
60
|
+
*/
|
|
61
|
+
readonly skills?: ReadonlyArray<HarnessV1Skill>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Instructions for the underlying agent runtime. Adapters prepend this to
|
|
65
|
+
* the first user message of a fresh session, once — it is not re-applied on
|
|
66
|
+
* later turns or when resuming a previously ended session.
|
|
67
|
+
*/
|
|
68
|
+
readonly instructions?: string;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Built-in tool permission mode. Defaults to `'allow-all'`, preserving the
|
|
72
|
+
* existing bypass-permissions behavior unless users opt in.
|
|
73
|
+
*/
|
|
74
|
+
readonly permissionMode?: HarnessV1PermissionMode;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Per custom-tool approval statuses. This mirrors AI SDK `toolApproval`
|
|
78
|
+
* object configuration for host-executed tools, without callback support.
|
|
79
|
+
*
|
|
80
|
+
* `not-applicable` and `approved` run the tool, `user-approval` pauses the
|
|
81
|
+
* turn for a user decision, and `denied` immediately submits an
|
|
82
|
+
* `execution-denied` result.
|
|
83
|
+
*/
|
|
84
|
+
readonly toolApproval?: HarnessAgentToolApprovalConfiguration;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sandbox provider whose `create()` produces the network sandbox session the
|
|
88
|
+
* harness runs against. Its `restricted()` view is also propagated to user
|
|
89
|
+
* tool `execute()` calls (as the `experimental_sandbox` field), typed as
|
|
90
|
+
* `Experimental_SandboxSession` so tools cannot reach the infra surface.
|
|
91
|
+
*/
|
|
92
|
+
readonly sandbox: HarnessV1SandboxProvider;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Called after each sandbox session is acquired and the session work
|
|
96
|
+
* directory exists, before the harness adapter starts. Runs for fresh and
|
|
97
|
+
* resumed sessions.
|
|
98
|
+
*
|
|
99
|
+
* Use this to write per-session config, install lightweight tools, activate
|
|
100
|
+
* licenses, or prepare files in `sessionWorkDir`. Keep it idempotent if the
|
|
101
|
+
* agent may resume sessions.
|
|
102
|
+
*/
|
|
103
|
+
readonly onSandboxSession?: (opts: {
|
|
104
|
+
readonly session: SandboxSession;
|
|
105
|
+
readonly sessionWorkDir: string;
|
|
106
|
+
readonly abortSignal?: AbortSignal;
|
|
107
|
+
}) => Promise<void>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Telemetry configuration. The harness drives AI SDK's pluggable
|
|
111
|
+
* `Telemetry` integration contract from the turn lifecycle, so a harness turn
|
|
112
|
+
* appears in a consumer's traces with the same span shape as `streamText`.
|
|
113
|
+
* Register an integration here (e.g. `@ai-sdk/otel`) or globally via
|
|
114
|
+
* `registerTelemetry`. The harness itself stays OpenTelemetry-agnostic.
|
|
115
|
+
*/
|
|
116
|
+
readonly telemetry?: TelemetryOptions;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Diagnostics configuration. Enables bridge log forwarding (sandbox
|
|
120
|
+
* console + structured `debug-event`s) and the `HARNESS_DEBUG` stderr default.
|
|
121
|
+
* Set `{ enabled: true }` to turn it on in code; env vars fill unset fields.
|
|
122
|
+
*/
|
|
123
|
+
readonly debug?: HarnessDebugConfig;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Programmatic sink for forwarded bridge diagnostics. Receives every
|
|
127
|
+
* captured console line and structured event, normalized. Independent of the
|
|
128
|
+
* stderr default — wire this to capture diagnostics in code.
|
|
129
|
+
*/
|
|
130
|
+
readonly onLog?: (event: HarnessDiagnostic) => void;
|
|
131
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { HarnessError } from '../errors/harness-error';
|
|
2
|
+
import type {
|
|
3
|
+
ModelMessage,
|
|
4
|
+
ToolApprovalRequest,
|
|
5
|
+
ToolApprovalResponse,
|
|
6
|
+
} from '@ai-sdk/provider-utils';
|
|
7
|
+
|
|
8
|
+
export type HarnessAgentToolApprovalContinuation = {
|
|
9
|
+
readonly approvalResponse: ToolApprovalResponse;
|
|
10
|
+
readonly toolCall: {
|
|
11
|
+
readonly type: 'tool-call';
|
|
12
|
+
readonly toolCallId: string;
|
|
13
|
+
readonly toolName: string;
|
|
14
|
+
readonly input: unknown;
|
|
15
|
+
readonly providerExecuted?: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract approval decisions that should continue a suspended harness turn.
|
|
21
|
+
*
|
|
22
|
+
* AI SDK clients send approval decisions as a trailing `role: "tool"` message
|
|
23
|
+
* containing `tool-approval-response` parts. The response only carries the
|
|
24
|
+
* approval id, so the harness has to recover the matching approval request
|
|
25
|
+
* locally to find the original tool call before it can resume the paused turn.
|
|
26
|
+
* Responses that already have a tool result are ignored, because those
|
|
27
|
+
* approvals were already consumed by a prior continuation.
|
|
28
|
+
*/
|
|
29
|
+
export function collectHarnessAgentToolApprovalContinuations(input: {
|
|
30
|
+
messages: readonly ModelMessage[];
|
|
31
|
+
}): readonly HarnessAgentToolApprovalContinuation[] {
|
|
32
|
+
const lastMessage = input.messages.at(-1);
|
|
33
|
+
if (lastMessage?.role !== 'tool') return [];
|
|
34
|
+
|
|
35
|
+
const toolCallsByToolCallId = new Map<
|
|
36
|
+
string,
|
|
37
|
+
HarnessAgentToolApprovalContinuation['toolCall']
|
|
38
|
+
>();
|
|
39
|
+
const approvalRequestsByApprovalId = new Map<string, ToolApprovalRequest>();
|
|
40
|
+
for (const message of input.messages) {
|
|
41
|
+
if (message.role !== 'assistant' || typeof message.content === 'string') {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const part of message.content) {
|
|
45
|
+
if (part.type === 'tool-call') {
|
|
46
|
+
toolCallsByToolCallId.set(part.toolCallId, {
|
|
47
|
+
type: 'tool-call',
|
|
48
|
+
toolCallId: part.toolCallId,
|
|
49
|
+
toolName: part.toolName,
|
|
50
|
+
input: part.input,
|
|
51
|
+
...(part.providerExecuted !== undefined
|
|
52
|
+
? { providerExecuted: part.providerExecuted }
|
|
53
|
+
: {}),
|
|
54
|
+
});
|
|
55
|
+
} else if (part.type === 'tool-approval-request') {
|
|
56
|
+
approvalRequestsByApprovalId.set(part.approvalId, part);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const toolResultIds = new Set<string>();
|
|
62
|
+
for (const part of lastMessage.content) {
|
|
63
|
+
if (part.type === 'tool-result') {
|
|
64
|
+
toolResultIds.add(part.toolCallId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const continuations: HarnessAgentToolApprovalContinuation[] = [];
|
|
69
|
+
for (const part of lastMessage.content) {
|
|
70
|
+
if (part.type !== 'tool-approval-response') continue;
|
|
71
|
+
|
|
72
|
+
const approvalRequest = approvalRequestsByApprovalId.get(part.approvalId);
|
|
73
|
+
if (approvalRequest == null) {
|
|
74
|
+
throw new HarnessError({
|
|
75
|
+
message: `Tool approval response '${part.approvalId}' does not match a prior tool approval request.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (toolResultIds.has(approvalRequest.toolCallId)) continue;
|
|
79
|
+
|
|
80
|
+
const toolCall = toolCallsByToolCallId.get(approvalRequest.toolCallId);
|
|
81
|
+
if (toolCall == null) {
|
|
82
|
+
throw new HarnessError({
|
|
83
|
+
message: `Tool approval request '${approvalRequest.approvalId}' references unknown tool call '${approvalRequest.toolCallId}'.`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
continuations.push({
|
|
88
|
+
approvalResponse: part,
|
|
89
|
+
toolCall,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return continuations;
|
|
94
|
+
}
|