@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,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Host/consumer-facing observability types.
|
|
3
|
+
*
|
|
4
|
+
* These are deliberately NOT part of the `HarnessV1` adapter spec (`src/v1`):
|
|
5
|
+
* no harness adapter implements or consumes them. Adapters only deal with the
|
|
6
|
+
* wire frames (`HarnessV1BridgeSandboxLog` / `HarnessV1BridgeDebugEvent`) and
|
|
7
|
+
* the `HarnessV1Observability` handle the framework hands to `doStart`. The
|
|
8
|
+
* framework normalizes those frames into the `HarnessDiagnostic` shape below
|
|
9
|
+
* for consumers (`HarnessAgentSettings.onLog`) and reporters. Hence no `V1`
|
|
10
|
+
* affix and a home outside the spec.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Severity of a diagnostic. */
|
|
14
|
+
export type HarnessDebugLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Consumer-facing diagnostics configuration. Set on `HarnessAgentSettings` to
|
|
18
|
+
* enable bridge log forwarding and the `HARNESS_DEBUG` stderr default in code.
|
|
19
|
+
* `HARNESS_DEBUG` / `HARNESS_DEBUG_LEVEL` / `HARNESS_DEBUG_SUBSYSTEMS` env vars
|
|
20
|
+
* fill any unset field — a convenience default, never the only path.
|
|
21
|
+
*/
|
|
22
|
+
export type HarnessDebugConfig = {
|
|
23
|
+
/** Master switch. Nothing is captured or forwarded when false/unset. */
|
|
24
|
+
readonly enabled?: boolean;
|
|
25
|
+
/** Threshold; events at or above this severity are emitted. Default `debug`. */
|
|
26
|
+
readonly level?: HarnessDebugLevel;
|
|
27
|
+
/** Dotted-prefix subsystem filter for structured events. */
|
|
28
|
+
readonly subsystems?: ReadonlyArray<string>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A forwarded bridge diagnostic, normalized for host consumers.
|
|
33
|
+
*
|
|
34
|
+
* The bridge emits two raw frame kinds — captured console lines (`sandbox-log`)
|
|
35
|
+
* and structured events (`debug-event`). The framework normalizes both into
|
|
36
|
+
* this single shape before handing them to a consumer's `onLog` callback, the
|
|
37
|
+
* `HARNESS_DEBUG` stderr default, and observability reporters. Diagnostics are
|
|
38
|
+
* kept first-class and per-line — they are never folded into telemetry spans.
|
|
39
|
+
*/
|
|
40
|
+
export type HarnessDiagnostic = {
|
|
41
|
+
/**
|
|
42
|
+
* Severity. Structured events carry their own level; captured console lines
|
|
43
|
+
* map `stderr` → `'warn'` and `stdout` → `'info'`.
|
|
44
|
+
*/
|
|
45
|
+
readonly level: HarnessDebugLevel;
|
|
46
|
+
|
|
47
|
+
/** Human-readable line (console capture) or message (structured event). */
|
|
48
|
+
readonly message: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Dotted subsystem. For captured console output this is
|
|
52
|
+
* `sandbox.log.<source>`; for structured events it is the adapter-supplied
|
|
53
|
+
* subsystem (e.g. `bridge.turn`).
|
|
54
|
+
*/
|
|
55
|
+
readonly subsystem: string;
|
|
56
|
+
|
|
57
|
+
/** `'log'` = captured console line; `'event'` = structured `bridgeLog`. */
|
|
58
|
+
readonly kind: 'log' | 'event';
|
|
59
|
+
|
|
60
|
+
/** Originating sandbox source label (console capture). */
|
|
61
|
+
readonly source?: string;
|
|
62
|
+
|
|
63
|
+
/** Which standard stream the line came from (console capture). */
|
|
64
|
+
readonly stream?: 'stdout' | 'stderr';
|
|
65
|
+
|
|
66
|
+
/** Structured attributes (structured events only). */
|
|
67
|
+
readonly attrs?: Record<string, unknown>;
|
|
68
|
+
|
|
69
|
+
/** Error payload (structured events only). */
|
|
70
|
+
readonly error?: { name?: string; message: string; stack?: string };
|
|
71
|
+
|
|
72
|
+
/** The harness session this diagnostic originated from. */
|
|
73
|
+
readonly sessionId?: string;
|
|
74
|
+
|
|
75
|
+
/** Host receipt time (epoch ms). */
|
|
76
|
+
readonly timestamp: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A telemetry integration that also wants the per-line diagnostics stream. The
|
|
81
|
+
* framework calls `ingestDiagnostic` for every forwarded bridge diagnostic in
|
|
82
|
+
* addition to driving the standard `Telemetry` span lifecycle, so a single
|
|
83
|
+
* reporter object (e.g. `createFileReporter`) registered in
|
|
84
|
+
* `telemetry.integrations` receives both spans and logs.
|
|
85
|
+
*/
|
|
86
|
+
export interface HarnessDiagnosticConsumer {
|
|
87
|
+
ingestDiagnostic?(diagnostic: HarnessDiagnostic): void;
|
|
88
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
|
+
import type { HarnessV1Bootstrap } from '../../v1';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Version of the bootstrap recipe shape itself. Bump to force every existing
|
|
6
|
+
* snapshot/marker to be invalidated regardless of recipe content.
|
|
7
|
+
*/
|
|
8
|
+
export const BOOTSTRAP_SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Deterministic 16-char hex identity derived from the recipe's content
|
|
12
|
+
* (harnessId, bootstrapDir, file paths + contents, commands, schema version).
|
|
13
|
+
* Two adapters with equivalent recipes produce the same identity; any
|
|
14
|
+
* content change produces a different identity.
|
|
15
|
+
*
|
|
16
|
+
* Used by sandbox providers as part of the persistent sandbox name so
|
|
17
|
+
* recipe changes automatically invalidate snapshots.
|
|
18
|
+
*/
|
|
19
|
+
export async function hashBootstrap(
|
|
20
|
+
recipe: HarnessV1Bootstrap,
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
const encoder = new TextEncoder();
|
|
23
|
+
const chunks: Uint8Array[] = [];
|
|
24
|
+
const pushString = (value: string) => {
|
|
25
|
+
chunks.push(encoder.encode(value));
|
|
26
|
+
chunks.push(encoder.encode('\0'));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
pushString(recipe.harnessId);
|
|
30
|
+
pushString(recipe.bootstrapDir);
|
|
31
|
+
|
|
32
|
+
const sortedFiles = [...recipe.files].sort((a, b) =>
|
|
33
|
+
a.path.localeCompare(b.path),
|
|
34
|
+
);
|
|
35
|
+
for (const file of sortedFiles) {
|
|
36
|
+
pushString(file.path);
|
|
37
|
+
pushString(file.content);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pushString(JSON.stringify(recipe.commands));
|
|
41
|
+
pushString(String(BOOTSTRAP_SCHEMA_VERSION));
|
|
42
|
+
|
|
43
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
44
|
+
const buffer = new Uint8Array(totalLength);
|
|
45
|
+
let offset = 0;
|
|
46
|
+
for (const chunk of chunks) {
|
|
47
|
+
buffer.set(chunk, offset);
|
|
48
|
+
offset += chunk.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const digest = await crypto.subtle.digest('SHA-256', buffer);
|
|
52
|
+
const bytes = new Uint8Array(digest);
|
|
53
|
+
let hex = '';
|
|
54
|
+
for (let i = 0; i < 8; i++) {
|
|
55
|
+
hex += bytes[i].toString(16).padStart(2, '0');
|
|
56
|
+
}
|
|
57
|
+
return hex;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Absolute path of the marker file the framework writes after a recipe runs
|
|
62
|
+
* successfully. Presence of this path inside the sandbox indicates the
|
|
63
|
+
* recipe with the matching `identity` has already been applied.
|
|
64
|
+
*/
|
|
65
|
+
export function bootstrapMarkerPath(
|
|
66
|
+
recipe: HarnessV1Bootstrap,
|
|
67
|
+
identity: string,
|
|
68
|
+
): string {
|
|
69
|
+
return `${recipe.bootstrapDir}/.bootstrap-${identity}.ok`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Apply a bootstrap recipe to a sandbox session idempotently. Reads the
|
|
74
|
+
* marker file; if it exists, returns immediately. Otherwise writes the
|
|
75
|
+
* recipe's files, runs its commands sequentially, and writes the marker
|
|
76
|
+
* on success.
|
|
77
|
+
*
|
|
78
|
+
* Safe to call multiple times. For sandboxes that already contain the
|
|
79
|
+
* recipe (resumed from snapshot, reused across sessions, or applied by
|
|
80
|
+
* an earlier process) this is a single fast read.
|
|
81
|
+
*/
|
|
82
|
+
export async function applyBootstrapRecipe(
|
|
83
|
+
session: SandboxSession,
|
|
84
|
+
recipe: HarnessV1Bootstrap,
|
|
85
|
+
identity: string,
|
|
86
|
+
options?: { abortSignal?: AbortSignal },
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const markerPath = bootstrapMarkerPath(recipe, identity);
|
|
89
|
+
|
|
90
|
+
const existingMarker = await session.readTextFile({
|
|
91
|
+
path: markerPath,
|
|
92
|
+
abortSignal: options?.abortSignal,
|
|
93
|
+
});
|
|
94
|
+
if (existingMarker !== null) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const file of recipe.files) {
|
|
99
|
+
await session.writeTextFile({
|
|
100
|
+
path: file.path,
|
|
101
|
+
content: file.content,
|
|
102
|
+
abortSignal: options?.abortSignal,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const cmd of recipe.commands) {
|
|
107
|
+
const result = await session.run({
|
|
108
|
+
command: cmd.command,
|
|
109
|
+
workingDirectory: cmd.workingDirectory,
|
|
110
|
+
abortSignal: options?.abortSignal,
|
|
111
|
+
});
|
|
112
|
+
if (result.exitCode !== 0) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Bootstrap command failed for harness '${recipe.harnessId}' (exit ${result.exitCode}): ${cmd.command}\n${result.stderr || result.stdout}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await session.writeTextFile({
|
|
120
|
+
path: markerPath,
|
|
121
|
+
content: '',
|
|
122
|
+
abortSignal: options?.abortSignal,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide registry for bridge-port leases. Used when a sandbox provider
|
|
3
|
+
* wraps a caller-provided sandbox with a pre-declared port pool — each
|
|
4
|
+
* concurrent harness session leases one port from the pool, releases on
|
|
5
|
+
* session stop or destroy. Multiple sessions on the same provider instance
|
|
6
|
+
* share the same pool; different provider instances (even wrapping the same
|
|
7
|
+
* underlying sandbox) get independent registries.
|
|
8
|
+
*
|
|
9
|
+
* Sized to the typical case: one provider object passed to N HarnessAgents.
|
|
10
|
+
* Callers that need cross-process coordination must layer that on top.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type RegistryEntry = {
|
|
14
|
+
readonly pool: ReadonlyArray<number>;
|
|
15
|
+
readonly leases: Map<string, number>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const registries = new WeakMap<object, RegistryEntry>();
|
|
19
|
+
|
|
20
|
+
export function acquireBridgePort(options: {
|
|
21
|
+
poolKey: object;
|
|
22
|
+
pool: ReadonlyArray<number>;
|
|
23
|
+
sessionId: string;
|
|
24
|
+
}): number {
|
|
25
|
+
let entry = registries.get(options.poolKey);
|
|
26
|
+
if (entry == null) {
|
|
27
|
+
entry = { pool: options.pool, leases: new Map() };
|
|
28
|
+
registries.set(options.poolKey, entry);
|
|
29
|
+
}
|
|
30
|
+
const existing = entry.leases.get(options.sessionId);
|
|
31
|
+
if (existing != null) return existing;
|
|
32
|
+
|
|
33
|
+
const leased = new Set(entry.leases.values());
|
|
34
|
+
for (const port of entry.pool) {
|
|
35
|
+
if (!leased.has(port)) {
|
|
36
|
+
entry.leases.set(options.sessionId, port);
|
|
37
|
+
return port;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
`No available bridge port — pool of ${entry.pool.length} ports is fully leased.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function releaseBridgePort(options: {
|
|
46
|
+
poolKey: object;
|
|
47
|
+
sessionId: string;
|
|
48
|
+
}): void {
|
|
49
|
+
const entry = registries.get(options.poolKey);
|
|
50
|
+
if (entry == null) return;
|
|
51
|
+
entry.leases.delete(options.sessionId);
|
|
52
|
+
}
|