@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +414 -0
  2. package/LICENSE +13 -0
  3. package/README.md +176 -0
  4. package/agent/index.ts +56 -0
  5. package/bridge/index.ts +10 -0
  6. package/dist/agent/index.d.ts +1631 -0
  7. package/dist/agent/index.js +3491 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/bridge/index.d.ts +129 -0
  10. package/dist/bridge/index.js +482 -0
  11. package/dist/bridge/index.js.map +1 -0
  12. package/dist/index.d.ts +1587 -0
  13. package/dist/index.js +517 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/index.d.ts +329 -0
  16. package/dist/utils/index.js +1241 -0
  17. package/dist/utils/index.js.map +1 -0
  18. package/package.json +100 -0
  19. package/src/agent/harness-agent-session.ts +518 -0
  20. package/src/agent/harness-agent-settings.ts +187 -0
  21. package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
  22. package/src/agent/harness-agent-tool-types.ts +15 -0
  23. package/src/agent/harness-agent-types.ts +50 -0
  24. package/src/agent/harness-agent.ts +865 -0
  25. package/src/agent/internal/bootstrap-recipe.ts +124 -0
  26. package/src/agent/internal/bridge-port-registry.ts +52 -0
  27. package/src/agent/internal/harness-stream-text-result.ts +731 -0
  28. package/src/agent/internal/lifecycle-state-validation.ts +95 -0
  29. package/src/agent/internal/permission-mode.ts +50 -0
  30. package/src/agent/internal/resolve-observability.ts +128 -0
  31. package/src/agent/internal/run-prompt.ts +901 -0
  32. package/src/agent/internal/sandbox-bootstrap.ts +266 -0
  33. package/src/agent/internal/strip-work-dir.ts +68 -0
  34. package/src/agent/internal/to-harness-stream.ts +75 -0
  35. package/src/agent/internal/tool-filtering.ts +114 -0
  36. package/src/agent/internal/translate-stream-part.ts +221 -0
  37. package/src/agent/internal/turn-telemetry.ts +361 -0
  38. package/src/agent/observability/file-reporter.ts +206 -0
  39. package/src/agent/observability/index.ts +15 -0
  40. package/src/agent/observability/trace-tree-reporter.ts +122 -0
  41. package/src/agent/observability/types.ts +86 -0
  42. package/src/agent/prepare-harness-sandbox-template.ts +68 -0
  43. package/src/agent/prepare-sandbox-for-harness.ts +165 -0
  44. package/src/bridge/index.ts +797 -0
  45. package/src/errors/harness-capability-unsupported-error.ts +41 -0
  46. package/src/errors/harness-error.ts +22 -0
  47. package/src/index.ts +3 -0
  48. package/src/utils/ai-gateway-auth.ts +15 -0
  49. package/src/utils/bridge-diagnostics.ts +213 -0
  50. package/src/utils/bridge-ready.ts +277 -0
  51. package/src/utils/classify-disk-log.ts +43 -0
  52. package/src/utils/index.ts +31 -0
  53. package/src/utils/sandbox-channel.ts +525 -0
  54. package/src/utils/sandbox-home-dir.ts +22 -0
  55. package/src/utils/shell-quote.ts +3 -0
  56. package/src/utils/write-skills.ts +141 -0
  57. package/src/v1/harness-v1-bootstrap.ts +46 -0
  58. package/src/v1/harness-v1-bridge-protocol.ts +342 -0
  59. package/src/v1/harness-v1-builtin-tool.ts +138 -0
  60. package/src/v1/harness-v1-call-warning.ts +22 -0
  61. package/src/v1/harness-v1-diagnostic.ts +66 -0
  62. package/src/v1/harness-v1-lifecycle-state.ts +65 -0
  63. package/src/v1/harness-v1-metadata.ts +13 -0
  64. package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
  65. package/src/v1/harness-v1-observability.ts +20 -0
  66. package/src/v1/harness-v1-permission-mode.ts +11 -0
  67. package/src/v1/harness-v1-prompt-control.ts +41 -0
  68. package/src/v1/harness-v1-prompt.ts +11 -0
  69. package/src/v1/harness-v1-sandbox-provider.ts +76 -0
  70. package/src/v1/harness-v1-session.ts +280 -0
  71. package/src/v1/harness-v1-skill.ts +36 -0
  72. package/src/v1/harness-v1-stream-part.ts +363 -0
  73. package/src/v1/harness-v1-tool-filtering.ts +25 -0
  74. package/src/v1/harness-v1-tool-spec.ts +31 -0
  75. package/src/v1/harness-v1.ts +94 -0
  76. package/src/v1/index.ts +99 -0
  77. 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>>;