@ai-sdk/harness 0.0.0 → 1.0.0-beta.15

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 (67) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/LICENSE +13 -0
  3. package/README.md +142 -0
  4. package/agent/index.ts +47 -0
  5. package/bridge/index.ts +10 -0
  6. package/dist/agent/index.d.ts +1521 -0
  7. package/dist/agent/index.js +2958 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/bridge/index.d.ts +111 -0
  10. package/dist/bridge/index.js +415 -0
  11. package/dist/bridge/index.js.map +1 -0
  12. package/dist/index.d.ts +1536 -0
  13. package/dist/index.js +15834 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/index.d.ts +225 -0
  16. package/dist/utils/index.js +12148 -0
  17. package/dist/utils/index.js.map +1 -0
  18. package/package.json +99 -1
  19. package/src/agent/harness-agent-session.ts +509 -0
  20. package/src/agent/harness-agent-settings.ts +131 -0
  21. package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
  22. package/src/agent/harness-agent-types.ts +50 -0
  23. package/src/agent/harness-agent.ts +819 -0
  24. package/src/agent/internal/bootstrap-recipe.ts +124 -0
  25. package/src/agent/internal/bridge-port-registry.ts +52 -0
  26. package/src/agent/internal/harness-stream-text-result.ts +720 -0
  27. package/src/agent/internal/lifecycle-state-validation.ts +95 -0
  28. package/src/agent/internal/permission-mode.ts +50 -0
  29. package/src/agent/internal/resolve-observability.ts +128 -0
  30. package/src/agent/internal/run-prompt.ts +813 -0
  31. package/src/agent/internal/strip-work-dir.ts +68 -0
  32. package/src/agent/internal/to-harness-stream.ts +75 -0
  33. package/src/agent/internal/translate-stream-part.ts +221 -0
  34. package/src/agent/internal/turn-telemetry.ts +359 -0
  35. package/src/agent/observability/file-reporter.ts +206 -0
  36. package/src/agent/observability/index.ts +15 -0
  37. package/src/agent/observability/trace-tree-reporter.ts +122 -0
  38. package/src/agent/observability/types.ts +86 -0
  39. package/src/agent/prewarm.ts +47 -0
  40. package/src/bridge/index.ts +702 -0
  41. package/src/errors/harness-capability-unsupported-error.ts +41 -0
  42. package/src/errors/harness-error.ts +22 -0
  43. package/src/index.ts +3 -0
  44. package/src/utils/bridge-ready.ts +277 -0
  45. package/src/utils/classify-disk-log.ts +43 -0
  46. package/src/utils/index.ts +15 -0
  47. package/src/utils/sandbox-channel.ts +453 -0
  48. package/src/v1/harness-v1-bootstrap.ts +46 -0
  49. package/src/v1/harness-v1-bridge-protocol.ts +310 -0
  50. package/src/v1/harness-v1-builtin-tool.ts +138 -0
  51. package/src/v1/harness-v1-call-warning.ts +22 -0
  52. package/src/v1/harness-v1-diagnostic.ts +66 -0
  53. package/src/v1/harness-v1-lifecycle-state.ts +65 -0
  54. package/src/v1/harness-v1-metadata.ts +13 -0
  55. package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
  56. package/src/v1/harness-v1-observability.ts +20 -0
  57. package/src/v1/harness-v1-permission-mode.ts +11 -0
  58. package/src/v1/harness-v1-prompt-control.ts +41 -0
  59. package/src/v1/harness-v1-prompt.ts +11 -0
  60. package/src/v1/harness-v1-sandbox-provider.ts +76 -0
  61. package/src/v1/harness-v1-session.ts +272 -0
  62. package/src/v1/harness-v1-skill.ts +36 -0
  63. package/src/v1/harness-v1-stream-part.ts +363 -0
  64. package/src/v1/harness-v1-tool-spec.ts +31 -0
  65. package/src/v1/harness-v1.ts +83 -0
  66. package/src/v1/index.ts +93 -0
  67. package/utils/index.ts +1 -0
@@ -0,0 +1,41 @@
1
+ import { AISDKError } from '@ai-sdk/provider';
2
+ import { HarnessError } from './harness-error';
3
+
4
+ const name = 'AI_HarnessCapabilityUnsupportedError';
5
+ const marker = `vercel.ai.error.${name}`;
6
+ const symbol = Symbol.for(marker);
7
+
8
+ /**
9
+ * Thrown when a caller asks the harness to do something the adapter (or the
10
+ * supplied sandbox) does not support, e.g. requesting manual compaction from
11
+ * an adapter that only auto-compacts, or invoking `getPortUrl` on a sandbox
12
+ * that does not expose one.
13
+ *
14
+ * The caller supplies the full human-readable message. Optional `harnessId`
15
+ * is recorded as structured context for tooling.
16
+ */
17
+ export class HarnessCapabilityUnsupportedError extends HarnessError {
18
+ private readonly [symbol] = true;
19
+
20
+ readonly harnessId?: string;
21
+
22
+ constructor({
23
+ message,
24
+ harnessId,
25
+ cause,
26
+ }: {
27
+ message: string;
28
+ harnessId?: string;
29
+ cause?: unknown;
30
+ }) {
31
+ super({ message, cause });
32
+ Object.defineProperty(this, 'name', { value: name });
33
+ this.harnessId = harnessId;
34
+ }
35
+
36
+ static isInstance(
37
+ error: unknown,
38
+ ): error is HarnessCapabilityUnsupportedError {
39
+ return AISDKError.hasMarker(error, marker);
40
+ }
41
+ }
@@ -0,0 +1,22 @@
1
+ import { AISDKError } from '@ai-sdk/provider';
2
+
3
+ const name = 'AI_HarnessError';
4
+ const marker = `vercel.ai.error.${name}`;
5
+ const symbol = Symbol.for(marker);
6
+
7
+ /**
8
+ * Base error type for failures originating in or signalled by a harness
9
+ * adapter. Specific failure modes (e.g. unsupported capability) extend this
10
+ * class.
11
+ */
12
+ export class HarnessError extends AISDKError {
13
+ private readonly [symbol] = true;
14
+
15
+ constructor({ message, cause }: { message: string; cause?: unknown }) {
16
+ super({ name, message, cause });
17
+ }
18
+
19
+ static isInstance(error: unknown): error is HarnessError {
20
+ return AISDKError.hasMarker(error, marker);
21
+ }
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './v1';
2
+ export * from './errors/harness-error';
3
+ export * from './errors/harness-capability-unsupported-error';
@@ -0,0 +1,277 @@
1
+ import {
2
+ safeParseJSON,
3
+ type Experimental_SandboxProcess,
4
+ type Experimental_SandboxSession,
5
+ } from '@ai-sdk/provider-utils';
6
+ import { z } from 'zod/v4';
7
+ import { harnessV1BridgeReadySchema } from '../v1/harness-v1-bridge-protocol';
8
+
9
+ const bridgeMetaSchema = z.object({
10
+ type: z.string().optional(),
11
+ port: z.number().optional(),
12
+ state: z.string().optional(),
13
+ pid: z.number().optional(),
14
+ });
15
+
16
+ export type BridgeReadySource = 'stdout' | 'metadata';
17
+
18
+ export type BridgeReadyErrorContext = {
19
+ proc: Experimental_SandboxProcess;
20
+ stdoutTail: string[];
21
+ };
22
+
23
+ export type WaitForBridgeReadyOptions = {
24
+ proc: Experimental_SandboxProcess;
25
+ sandbox: Experimental_SandboxSession;
26
+ bridgeStateDir: string;
27
+ bridgeType: string;
28
+ timeoutMs: number;
29
+ abortSignal?: AbortSignal;
30
+ pollIntervalMs?: number;
31
+ createTimeoutError?: (
32
+ context: BridgeReadyErrorContext,
33
+ ) => Error | Promise<Error>;
34
+ createExitError?: (
35
+ context: BridgeReadyErrorContext,
36
+ ) => Error | Promise<Error>;
37
+ };
38
+
39
+ export type WaitForBridgeReadyResult = {
40
+ port: number;
41
+ source: BridgeReadySource;
42
+ stdoutTail: string[];
43
+ };
44
+
45
+ export async function markBridgeStarting({
46
+ sandbox,
47
+ bridgeStateDir,
48
+ bridgeType,
49
+ abortSignal,
50
+ }: {
51
+ sandbox: Experimental_SandboxSession;
52
+ bridgeStateDir: string;
53
+ bridgeType: string;
54
+ abortSignal?: AbortSignal;
55
+ }): Promise<void> {
56
+ try {
57
+ await sandbox.writeTextFile({
58
+ path: bridgeMetaPath(bridgeStateDir),
59
+ content: JSON.stringify({ type: bridgeType, state: 'starting' }),
60
+ abortSignal,
61
+ });
62
+ } catch {
63
+ /*
64
+ * The bridge's own metadata writes are best-effort. This host-side marker
65
+ * only prevents stale readiness fallback when possible; stdout remains the
66
+ * primary readiness signal.
67
+ */
68
+ }
69
+ }
70
+
71
+ export async function waitForBridgeReady({
72
+ proc,
73
+ sandbox,
74
+ bridgeStateDir,
75
+ bridgeType,
76
+ timeoutMs,
77
+ abortSignal,
78
+ pollIntervalMs = 100,
79
+ createTimeoutError,
80
+ createExitError,
81
+ }: WaitForBridgeReadyOptions): Promise<WaitForBridgeReadyResult> {
82
+ const reader = proc.stdout.pipeThrough(new TextDecoderStream()).getReader();
83
+ const decoder = lineDecoder();
84
+ const stdoutTail: string[] = [];
85
+ const deadline = Date.now() + timeoutMs;
86
+ let pendingStdoutRead: Promise<ReadableStreamReadResult<string>> | undefined;
87
+ let pendingMetaRead: Promise<number | undefined> | undefined;
88
+ let nextMetaReadAt = 0;
89
+ let cancelReader = false;
90
+
91
+ try {
92
+ while (true) {
93
+ if (abortSignal?.aborted) {
94
+ cancelReader = true;
95
+ await proc.kill();
96
+ throw abortSignal.reason ?? new DOMException('Aborted', 'AbortError');
97
+ }
98
+
99
+ const remaining = deadline - Date.now();
100
+ if (remaining <= 0) {
101
+ cancelReader = true;
102
+ await proc.kill();
103
+ throw await makeBridgeReadyError({
104
+ createError: createTimeoutError,
105
+ fallbackMessage: 'bridge did not become ready in time.',
106
+ proc,
107
+ stdoutTail,
108
+ });
109
+ }
110
+
111
+ pendingStdoutRead ??= reader.read();
112
+ const metaReadDelayMs = Math.max(0, nextMetaReadAt - Date.now());
113
+ if (pendingMetaRead === undefined && metaReadDelayMs === 0) {
114
+ pendingMetaRead = readBridgeMetaReady({
115
+ sandbox,
116
+ bridgeStateDir,
117
+ bridgeType,
118
+ abortSignal,
119
+ });
120
+ }
121
+
122
+ const result = await Promise.race([
123
+ pendingStdoutRead.then(read => ({ source: 'stdout' as const, read })),
124
+ ...(pendingMetaRead === undefined
125
+ ? []
126
+ : [
127
+ pendingMetaRead.then(port => ({
128
+ source: 'metadata' as const,
129
+ port,
130
+ })),
131
+ ]),
132
+ sleep(
133
+ Math.min(
134
+ remaining,
135
+ pendingMetaRead === undefined ? metaReadDelayMs : pollIntervalMs,
136
+ ),
137
+ ).then(() => undefined),
138
+ ]);
139
+
140
+ if (result === undefined) continue;
141
+
142
+ if (result.source === 'metadata') {
143
+ pendingMetaRead = undefined;
144
+ if (result.port !== undefined) {
145
+ cancelReader = true;
146
+ return {
147
+ port: result.port,
148
+ source: 'metadata',
149
+ stdoutTail: [...stdoutTail],
150
+ };
151
+ }
152
+ nextMetaReadAt = Date.now() + pollIntervalMs;
153
+ continue;
154
+ }
155
+
156
+ pendingStdoutRead = undefined;
157
+ const { value, done } = result.read;
158
+ if (done) {
159
+ for (const line of decoder.flush()) {
160
+ pushTail({ lines: stdoutTail, line });
161
+ }
162
+ throw await makeBridgeReadyError({
163
+ createError: createExitError,
164
+ fallbackMessage: 'bridge exited before becoming ready.',
165
+ proc,
166
+ stdoutTail,
167
+ });
168
+ }
169
+ if (value === undefined) continue;
170
+ for (const line of decoder.push(value)) {
171
+ pushTail({ lines: stdoutTail, line });
172
+ const parsed = await safeParseJSON({
173
+ text: line,
174
+ schema: harnessV1BridgeReadySchema,
175
+ });
176
+ if (parsed.success) {
177
+ return {
178
+ port: parsed.value.port,
179
+ source: 'stdout',
180
+ stdoutTail: [...stdoutTail],
181
+ };
182
+ }
183
+ }
184
+ }
185
+ } finally {
186
+ if (cancelReader) {
187
+ await Promise.race([reader.cancel().catch(() => {}), sleep(100)]);
188
+ }
189
+ try {
190
+ reader.releaseLock();
191
+ } catch {}
192
+ }
193
+ }
194
+
195
+ async function readBridgeMetaReady({
196
+ sandbox,
197
+ bridgeStateDir,
198
+ bridgeType,
199
+ abortSignal,
200
+ }: {
201
+ sandbox: Experimental_SandboxSession;
202
+ bridgeStateDir: string;
203
+ bridgeType: string;
204
+ abortSignal?: AbortSignal;
205
+ }): Promise<number | undefined> {
206
+ const raw = await Promise.resolve(
207
+ sandbox.readTextFile({
208
+ path: bridgeMetaPath(bridgeStateDir),
209
+ abortSignal,
210
+ }),
211
+ ).catch(() => null);
212
+ if (raw == null) return undefined;
213
+
214
+ const parsed = await safeParseJSON({ text: raw, schema: bridgeMetaSchema });
215
+ if (!parsed.success) return undefined;
216
+ if (parsed.value.type !== bridgeType) return undefined;
217
+ if (parsed.value.state !== 'waiting') return undefined;
218
+ if (parsed.value.port === undefined || parsed.value.port <= 0) {
219
+ return undefined;
220
+ }
221
+ return parsed.value.port;
222
+ }
223
+
224
+ async function makeBridgeReadyError({
225
+ createError,
226
+ fallbackMessage,
227
+ proc,
228
+ stdoutTail,
229
+ }: {
230
+ createError:
231
+ | ((context: BridgeReadyErrorContext) => Error | Promise<Error>)
232
+ | undefined;
233
+ fallbackMessage: string;
234
+ proc: Experimental_SandboxProcess;
235
+ stdoutTail: string[];
236
+ }): Promise<Error> {
237
+ if (createError) {
238
+ return createError({ proc, stdoutTail: [...stdoutTail] });
239
+ }
240
+ return new Error(fallbackMessage);
241
+ }
242
+
243
+ function bridgeMetaPath(bridgeStateDir: string): string {
244
+ return `${bridgeStateDir}/bridge-meta.json`;
245
+ }
246
+
247
+ function pushTail({ lines, line }: { lines: string[]; line: string }): void {
248
+ lines.push(line);
249
+ if (lines.length > 20) lines.shift();
250
+ }
251
+
252
+ function sleep(ms: number): Promise<void> {
253
+ return new Promise(resolve => setTimeout(resolve, ms));
254
+ }
255
+
256
+ function lineDecoder() {
257
+ let buffer = '';
258
+ return {
259
+ push(chunk: string): string[] {
260
+ buffer += chunk;
261
+ const lines: string[] = [];
262
+ let nl: number;
263
+ while ((nl = buffer.indexOf('\n')) !== -1) {
264
+ const raw = buffer.slice(0, nl);
265
+ buffer = buffer.slice(nl + 1);
266
+ const line = raw.replace(/\r$/, '').trim();
267
+ if (line.length > 0) lines.push(line);
268
+ }
269
+ return lines;
270
+ },
271
+ flush(): string[] {
272
+ const line = buffer.replace(/\r$/, '').trim();
273
+ buffer = '';
274
+ return line.length > 0 ? [line] : [];
275
+ },
276
+ };
277
+ }
@@ -0,0 +1,43 @@
1
+ import { safeParseJSON } from '@ai-sdk/provider-utils';
2
+
3
+ /**
4
+ * Recovery rung selected from an on-disk bridge event log when attach is not
5
+ * possible (the bridge process is gone): `'replay'` when the log holds a
6
+ * complete turn the host can resume from its cursor, `'rerun'` otherwise.
7
+ */
8
+ export type DiskLogRecoveryMode = 'replay' | 'rerun';
9
+
10
+ /**
11
+ * Decide whether a respawned bridge can `replay` a turn from its persisted
12
+ * `event-log.ndjson`, or must `rerun` it from scratch.
13
+ *
14
+ * A turn is replayable only when its log ends in a terminal `finish` event —
15
+ * a log that is missing, empty, or ends mid-turn means the bridge died before
16
+ * completing the turn, so there is no coherent tail to deliver and the runtime
17
+ * must re-run it (continuing its own thread from the sandbox snapshot).
18
+ *
19
+ * @param eventLog Raw contents of `event-log.ndjson` (newline-delimited JSON),
20
+ * or `null`/`undefined`/empty when the file is absent.
21
+ */
22
+ export async function classifyDiskLog(
23
+ eventLog: string | null | undefined,
24
+ ): Promise<DiskLogRecoveryMode> {
25
+ if (!eventLog) return 'rerun';
26
+ const lines = eventLog
27
+ .split('\n')
28
+ .map(line => line.trim())
29
+ .filter(Boolean);
30
+ const lastLine = lines.at(-1);
31
+ if (lastLine == null) return 'rerun';
32
+
33
+ const parsed = await safeParseJSON({ text: lastLine });
34
+ if (
35
+ parsed.success &&
36
+ parsed.value != null &&
37
+ typeof parsed.value === 'object' &&
38
+ (parsed.value as { type?: unknown }).type === 'finish'
39
+ ) {
40
+ return 'replay';
41
+ }
42
+ return 'rerun';
43
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ SandboxChannel,
3
+ type SandboxChannelDebugEvent,
4
+ type SandboxChannelOptions,
5
+ type SandboxChannelReconnectOptions,
6
+ } from './sandbox-channel';
7
+ export { classifyDiskLog, type DiskLogRecoveryMode } from './classify-disk-log';
8
+ export {
9
+ markBridgeStarting,
10
+ waitForBridgeReady,
11
+ type BridgeReadyErrorContext,
12
+ type BridgeReadySource,
13
+ type WaitForBridgeReadyOptions,
14
+ type WaitForBridgeReadyResult,
15
+ } from './bridge-ready';