@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,97 @@
|
|
|
1
|
+
import { Telemetry } from 'ai';
|
|
2
|
+
|
|
3
|
+
/** Severity of a diagnostic. */
|
|
4
|
+
type HarnessDebugLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
|
5
|
+
/**
|
|
6
|
+
* A forwarded bridge diagnostic, normalized for host consumers.
|
|
7
|
+
*
|
|
8
|
+
* The bridge emits two raw frame kinds — captured console lines (`sandbox-log`)
|
|
9
|
+
* and structured events (`debug-event`). The framework normalizes both into
|
|
10
|
+
* this single shape before handing them to a consumer's `onLog` callback, the
|
|
11
|
+
* `HARNESS_DEBUG` stderr default, and observability reporters. Diagnostics are
|
|
12
|
+
* kept first-class and per-line — they are never folded into telemetry spans.
|
|
13
|
+
*/
|
|
14
|
+
type HarnessDiagnostic = {
|
|
15
|
+
/**
|
|
16
|
+
* Severity. Structured events carry their own level; captured console lines
|
|
17
|
+
* map `stderr` → `'warn'` and `stdout` → `'info'`.
|
|
18
|
+
*/
|
|
19
|
+
readonly level: HarnessDebugLevel;
|
|
20
|
+
/** Human-readable line (console capture) or message (structured event). */
|
|
21
|
+
readonly message: string;
|
|
22
|
+
/**
|
|
23
|
+
* Dotted subsystem. For captured console output this is
|
|
24
|
+
* `sandbox.log.<source>`; for structured events it is the adapter-supplied
|
|
25
|
+
* subsystem (e.g. `bridge.turn`).
|
|
26
|
+
*/
|
|
27
|
+
readonly subsystem: string;
|
|
28
|
+
/** `'log'` = captured console line; `'event'` = structured `bridgeLog`. */
|
|
29
|
+
readonly kind: 'log' | 'event';
|
|
30
|
+
/** Originating sandbox source label (console capture). */
|
|
31
|
+
readonly source?: string;
|
|
32
|
+
/** Which standard stream the line came from (console capture). */
|
|
33
|
+
readonly stream?: 'stdout' | 'stderr';
|
|
34
|
+
/** Structured attributes (structured events only). */
|
|
35
|
+
readonly attrs?: Record<string, unknown>;
|
|
36
|
+
/** Error payload (structured events only). */
|
|
37
|
+
readonly error?: {
|
|
38
|
+
name?: string;
|
|
39
|
+
message: string;
|
|
40
|
+
stack?: string;
|
|
41
|
+
};
|
|
42
|
+
/** The harness session this diagnostic originated from. */
|
|
43
|
+
readonly sessionId?: string;
|
|
44
|
+
/** Host receipt time (epoch ms). */
|
|
45
|
+
readonly timestamp: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A telemetry integration that also wants the per-line diagnostics stream. The
|
|
49
|
+
* framework calls `ingestDiagnostic` for every forwarded bridge diagnostic in
|
|
50
|
+
* addition to driving the standard `Telemetry` span lifecycle, so a single
|
|
51
|
+
* reporter object (e.g. `createFileReporter`) registered in
|
|
52
|
+
* `telemetry.integrations` receives both spans and logs.
|
|
53
|
+
*/
|
|
54
|
+
interface HarnessDiagnosticConsumer {
|
|
55
|
+
ingestDiagnostic?(diagnostic: HarnessDiagnostic): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A harness observability reporter that writes a unified, non-lossy
|
|
60
|
+
* `events.jsonl` containing **both** the telemetry span lifecycle (turn / step
|
|
61
|
+
* / tool) **and** the forwarded bridge diagnostics (console lines + structured
|
|
62
|
+
* events). It is a single object registered in `telemetry.integrations`: the
|
|
63
|
+
* framework drives its `Telemetry` methods for spans and calls
|
|
64
|
+
* `ingestDiagnostic` for diagnostics.
|
|
65
|
+
*
|
|
66
|
+
* No external collector or OTel setup required — this is the AI-SDK-idiomatic
|
|
67
|
+
* replacement for the original SDK's host-side artifact files.
|
|
68
|
+
*/
|
|
69
|
+
interface FileReporterOptions {
|
|
70
|
+
/** Directory for `events.jsonl` (created if absent). */
|
|
71
|
+
dir: string;
|
|
72
|
+
/**
|
|
73
|
+
* Buffer a turn's records in memory and write them only if the turn produced
|
|
74
|
+
* an error (an `error`-level diagnostic, a failed tool, or an error finish).
|
|
75
|
+
* Default false (write everything).
|
|
76
|
+
*/
|
|
77
|
+
failOnly?: boolean;
|
|
78
|
+
/** File name within `dir`. Default `events.jsonl`. */
|
|
79
|
+
fileName?: string;
|
|
80
|
+
}
|
|
81
|
+
type FileReporter = Telemetry & HarnessDiagnosticConsumer;
|
|
82
|
+
declare function createFileReporter(options: FileReporterOptions): FileReporter;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A harness observability reporter that renders an ASCII trace tree of a
|
|
86
|
+
* turn's span lifecycle (turn → steps → tools) to a stream at turn end. It is a
|
|
87
|
+
* `Telemetry` integration — register it in `telemetry.integrations`. Useful for
|
|
88
|
+
* zero-setup local debugging when no OTel collector is wired up; a real OTel
|
|
89
|
+
* backend (via `@ai-sdk/otel`) is a strict superset.
|
|
90
|
+
*/
|
|
91
|
+
interface TraceTreeReporterOptions {
|
|
92
|
+
/** Where to write the rendered tree. Default `process.stderr.write`. */
|
|
93
|
+
write?: (chunk: string) => void;
|
|
94
|
+
}
|
|
95
|
+
declare function createTraceTreeReporter(options?: TraceTreeReporterOptions): Telemetry;
|
|
96
|
+
|
|
97
|
+
export { type FileReporter, type FileReporterOptions, type HarnessDiagnostic, type HarnessDiagnosticConsumer, type TraceTreeReporterOptions, createFileReporter, createTraceTreeReporter };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/observability/file-reporter.ts
|
|
2
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
3
|
+
function createFileReporter(options) {
|
|
4
|
+
var _a, _b;
|
|
5
|
+
const fileName = (_a = options.fileName) != null ? _a : "events.jsonl";
|
|
6
|
+
const path = `${options.dir}/${fileName}`;
|
|
7
|
+
const failOnly = (_b = options.failOnly) != null ? _b : false;
|
|
8
|
+
const turns = /* @__PURE__ */ new Map();
|
|
9
|
+
let lastOpenCallId;
|
|
10
|
+
let dirReady = false;
|
|
11
|
+
const bucketFor = (callId) => {
|
|
12
|
+
let bucket = turns.get(callId);
|
|
13
|
+
if (!bucket) {
|
|
14
|
+
bucket = { lines: [], errored: false };
|
|
15
|
+
turns.set(callId, bucket);
|
|
16
|
+
}
|
|
17
|
+
return bucket;
|
|
18
|
+
};
|
|
19
|
+
const record = (callId, rec) => {
|
|
20
|
+
const id = callId != null ? callId : lastOpenCallId;
|
|
21
|
+
if (id == null) {
|
|
22
|
+
flushLines([rec]);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
bucketFor(id).lines.push(rec);
|
|
26
|
+
};
|
|
27
|
+
const flushLines = (lines) => {
|
|
28
|
+
if (lines.length === 0) return;
|
|
29
|
+
try {
|
|
30
|
+
if (!dirReady) {
|
|
31
|
+
mkdirSync(options.dir, { recursive: true });
|
|
32
|
+
dirReady = true;
|
|
33
|
+
}
|
|
34
|
+
appendFileSync(path, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
|
|
35
|
+
} catch (e) {
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const finishTurn = (callId) => {
|
|
39
|
+
const bucket = turns.get(callId);
|
|
40
|
+
if (!bucket) return;
|
|
41
|
+
turns.delete(callId);
|
|
42
|
+
if (lastOpenCallId === callId) lastOpenCallId = void 0;
|
|
43
|
+
if (failOnly && !bucket.errored) return;
|
|
44
|
+
flushLines(bucket.lines);
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
onStart(event) {
|
|
48
|
+
const e = event;
|
|
49
|
+
lastOpenCallId = e.callId;
|
|
50
|
+
record(e.callId, {
|
|
51
|
+
ts: Date.now(),
|
|
52
|
+
kind: "turn-start",
|
|
53
|
+
callId: e.callId,
|
|
54
|
+
operationId: e.operationId,
|
|
55
|
+
provider: e.provider,
|
|
56
|
+
modelId: e.modelId,
|
|
57
|
+
// Input prompt, unless the consumer opted out via `recordInputs: false`.
|
|
58
|
+
...e.recordInputs === false ? {} : { input: { messages: e.messages, instructions: e.instructions } }
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
onStepStart(event) {
|
|
62
|
+
const e = event;
|
|
63
|
+
record(e.callId, {
|
|
64
|
+
ts: Date.now(),
|
|
65
|
+
kind: "step-start",
|
|
66
|
+
callId: e.callId,
|
|
67
|
+
step: e.stepNumber
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
onToolExecutionStart(event) {
|
|
71
|
+
const e = event;
|
|
72
|
+
record(e.callId, {
|
|
73
|
+
ts: Date.now(),
|
|
74
|
+
kind: "tool-start",
|
|
75
|
+
callId: e.callId,
|
|
76
|
+
toolName: e.toolCall.toolName,
|
|
77
|
+
toolCallId: e.toolCall.toolCallId,
|
|
78
|
+
input: e.toolCall.input
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
onToolExecutionEnd(event) {
|
|
82
|
+
const e = event;
|
|
83
|
+
const isError = e.toolOutput.type === "error";
|
|
84
|
+
if (isError) bucketFor(e.callId).errored = true;
|
|
85
|
+
record(e.callId, {
|
|
86
|
+
ts: Date.now(),
|
|
87
|
+
kind: "tool-end",
|
|
88
|
+
callId: e.callId,
|
|
89
|
+
toolCallId: e.toolCall.toolCallId,
|
|
90
|
+
isError,
|
|
91
|
+
output: isError ? e.toolOutput.error : e.toolOutput.output
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
onStepFinish(event) {
|
|
95
|
+
const e = event;
|
|
96
|
+
record(e.callId, {
|
|
97
|
+
ts: Date.now(),
|
|
98
|
+
kind: "step-finish",
|
|
99
|
+
callId: e.callId,
|
|
100
|
+
usage: e.usage,
|
|
101
|
+
// The model's output content, unless `recordOutputs: false`.
|
|
102
|
+
...e.recordOutputs === false || e.content == null ? {} : { output: e.content }
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
onEnd(event) {
|
|
106
|
+
var _a2;
|
|
107
|
+
const e = event;
|
|
108
|
+
record(e.callId, {
|
|
109
|
+
ts: Date.now(),
|
|
110
|
+
kind: "turn-finish",
|
|
111
|
+
callId: e.callId,
|
|
112
|
+
finishReason: e.finishReason,
|
|
113
|
+
usage: (_a2 = e.totalUsage) != null ? _a2 : e.usage
|
|
114
|
+
});
|
|
115
|
+
finishTurn(e.callId);
|
|
116
|
+
},
|
|
117
|
+
onError(error) {
|
|
118
|
+
if (lastOpenCallId != null) bucketFor(lastOpenCallId).errored = true;
|
|
119
|
+
record(lastOpenCallId, {
|
|
120
|
+
ts: Date.now(),
|
|
121
|
+
kind: "error",
|
|
122
|
+
error: error instanceof Error ? { name: error.name, message: error.message } : error
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
ingestDiagnostic(diagnostic) {
|
|
126
|
+
if (diagnostic.level === "error" && lastOpenCallId != null) {
|
|
127
|
+
bucketFor(lastOpenCallId).errored = true;
|
|
128
|
+
}
|
|
129
|
+
record(lastOpenCallId, {
|
|
130
|
+
ts: diagnostic.timestamp,
|
|
131
|
+
kind: "diagnostic",
|
|
132
|
+
diagnostic
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/observability/trace-tree-reporter.ts
|
|
139
|
+
function createTraceTreeReporter(options = {}) {
|
|
140
|
+
var _a;
|
|
141
|
+
const write = (_a = options.write) != null ? _a : ((chunk) => void process.stderr.write(chunk));
|
|
142
|
+
const turns = /* @__PURE__ */ new Map();
|
|
143
|
+
const render = (node, depth) => {
|
|
144
|
+
const indent = " ".repeat(depth);
|
|
145
|
+
const dur = node.endMs != null ? `${Math.max(0, Math.round(node.endMs - node.startMs))}ms` : "(open)";
|
|
146
|
+
let out = `${indent}- ${node.label} ${dur}
|
|
147
|
+
`;
|
|
148
|
+
for (const child of node.children) out += render(child, depth + 1);
|
|
149
|
+
return out;
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
onStart(event) {
|
|
153
|
+
var _a2;
|
|
154
|
+
const e = event;
|
|
155
|
+
turns.set(e.callId, {
|
|
156
|
+
root: {
|
|
157
|
+
label: `${(_a2 = e.operationId) != null ? _a2 : "turn"}${e.modelId ? ` ${e.modelId}` : ""}`,
|
|
158
|
+
startMs: Date.now(),
|
|
159
|
+
children: []
|
|
160
|
+
},
|
|
161
|
+
tools: /* @__PURE__ */ new Map()
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
onStepStart(event) {
|
|
165
|
+
var _a2;
|
|
166
|
+
const e = event;
|
|
167
|
+
const turn = turns.get(e.callId);
|
|
168
|
+
if (!turn) return;
|
|
169
|
+
const step = {
|
|
170
|
+
label: `step ${((_a2 = e.stepNumber) != null ? _a2 : turn.root.children.length) + 1}`,
|
|
171
|
+
startMs: Date.now(),
|
|
172
|
+
children: []
|
|
173
|
+
};
|
|
174
|
+
turn.step = step;
|
|
175
|
+
turn.root.children.push(step);
|
|
176
|
+
},
|
|
177
|
+
onToolExecutionStart(event) {
|
|
178
|
+
var _a2;
|
|
179
|
+
const e = event;
|
|
180
|
+
const turn = turns.get(e.callId);
|
|
181
|
+
const parent = (_a2 = turn == null ? void 0 : turn.step) != null ? _a2 : turn == null ? void 0 : turn.root;
|
|
182
|
+
if (!turn || !parent) return;
|
|
183
|
+
const node = {
|
|
184
|
+
label: `tool ${e.toolCall.toolName}`,
|
|
185
|
+
startMs: Date.now(),
|
|
186
|
+
children: []
|
|
187
|
+
};
|
|
188
|
+
turn.tools.set(e.toolCall.toolCallId, node);
|
|
189
|
+
parent.children.push(node);
|
|
190
|
+
},
|
|
191
|
+
onToolExecutionEnd(event) {
|
|
192
|
+
var _a2;
|
|
193
|
+
const e = event;
|
|
194
|
+
const node = (_a2 = turns.get(e.callId)) == null ? void 0 : _a2.tools.get(e.toolCall.toolCallId);
|
|
195
|
+
if (!node) return;
|
|
196
|
+
node.endMs = Date.now();
|
|
197
|
+
if (e.toolOutput.type === "error") node.label += " [error]";
|
|
198
|
+
},
|
|
199
|
+
onStepFinish(event) {
|
|
200
|
+
const e = event;
|
|
201
|
+
const turn = turns.get(e.callId);
|
|
202
|
+
if (turn == null ? void 0 : turn.step) {
|
|
203
|
+
turn.step.endMs = Date.now();
|
|
204
|
+
turn.step = void 0;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
onEnd(event) {
|
|
208
|
+
const e = event;
|
|
209
|
+
const turn = turns.get(e.callId);
|
|
210
|
+
if (!turn) return;
|
|
211
|
+
turn.root.endMs = Date.now();
|
|
212
|
+
turns.delete(e.callId);
|
|
213
|
+
try {
|
|
214
|
+
write(`
|
|
215
|
+
${render(turn.root, 0)}`);
|
|
216
|
+
} catch (e2) {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export {
|
|
222
|
+
createFileReporter,
|
|
223
|
+
createTraceTreeReporter
|
|
224
|
+
};
|
|
225
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/observability/file-reporter.ts","../../src/observability/trace-tree-reporter.ts"],"sourcesContent":["import { appendFileSync, mkdirSync } from 'node:fs';\nimport type { Telemetry } from 'ai';\nimport type {\n HarnessDiagnostic,\n HarnessDiagnosticConsumer,\n} from '../agent/harness-diagnostics';\n\n/**\n * A harness observability reporter that writes a unified, non-lossy\n * `events.jsonl` containing **both** the telemetry span lifecycle (turn / step\n * / tool) **and** the forwarded bridge diagnostics (console lines + structured\n * events). It is a single object registered in `telemetry.integrations`: the\n * framework drives its `Telemetry` methods for spans and calls\n * `ingestDiagnostic` for diagnostics.\n *\n * No external collector or OTel setup required — this is the AI-SDK-idiomatic\n * replacement for the original SDK's host-side artifact files.\n */\nexport interface FileReporterOptions {\n /** Directory for `events.jsonl` (created if absent). */\n dir: string;\n /**\n * Buffer a turn's records in memory and write them only if the turn produced\n * an error (an `error`-level diagnostic, a failed tool, or an error finish).\n * Default false (write everything).\n */\n failOnly?: boolean;\n /** File name within `dir`. Default `events.jsonl`. */\n fileName?: string;\n}\n\ntype Record_ = { ts: number } & Record<string, unknown>;\n\nexport type FileReporter = Telemetry & HarnessDiagnosticConsumer;\n\nexport function createFileReporter(options: FileReporterOptions): FileReporter {\n const fileName = options.fileName ?? 'events.jsonl';\n const path = `${options.dir}/${fileName}`;\n const failOnly = options.failOnly ?? false;\n\n // Per-turn buffers, keyed by the telemetry callId. Diagnostics (which carry a\n // sessionId, not a callId) attach to the most recently started, open turn.\n const turns = new Map<string, { lines: Record_[]; errored: boolean }>();\n let lastOpenCallId: string | undefined;\n let dirReady = false;\n\n const bucketFor = (callId: string) => {\n let bucket = turns.get(callId);\n if (!bucket) {\n bucket = { lines: [], errored: false };\n turns.set(callId, bucket);\n }\n return bucket;\n };\n\n const record = (callId: string | undefined, rec: Record_): void => {\n const id = callId ?? lastOpenCallId;\n if (id == null) {\n // No active turn — write standalone (best-effort).\n flushLines([rec]);\n return;\n }\n bucketFor(id).lines.push(rec);\n };\n\n const flushLines = (lines: Record_[]): void => {\n if (lines.length === 0) return;\n try {\n if (!dirReady) {\n mkdirSync(options.dir, { recursive: true });\n dirReady = true;\n }\n appendFileSync(path, lines.map(l => JSON.stringify(l)).join('\\n') + '\\n');\n } catch {\n // Best-effort: never let observability break a turn.\n }\n };\n\n const finishTurn = (callId: string): void => {\n const bucket = turns.get(callId);\n if (!bucket) return;\n turns.delete(callId);\n if (lastOpenCallId === callId) lastOpenCallId = undefined;\n if (failOnly && !bucket.errored) return;\n flushLines(bucket.lines);\n };\n\n return {\n onStart(event) {\n const e = event as {\n callId: string;\n operationId?: string;\n modelId?: string;\n provider?: string;\n messages?: unknown;\n instructions?: unknown;\n recordInputs?: boolean;\n };\n lastOpenCallId = e.callId;\n record(e.callId, {\n ts: Date.now(),\n kind: 'turn-start',\n callId: e.callId,\n operationId: e.operationId,\n provider: e.provider,\n modelId: e.modelId,\n // Input prompt, unless the consumer opted out via `recordInputs: false`.\n ...(e.recordInputs === false\n ? {}\n : { input: { messages: e.messages, instructions: e.instructions } }),\n });\n },\n onStepStart(event) {\n const e = event as { callId: string; stepNumber?: number };\n record(e.callId, {\n ts: Date.now(),\n kind: 'step-start',\n callId: e.callId,\n step: e.stepNumber,\n });\n },\n onToolExecutionStart(event) {\n const e = event as {\n callId: string;\n toolCall: { toolName: string; toolCallId: string; input: unknown };\n };\n record(e.callId, {\n ts: Date.now(),\n kind: 'tool-start',\n callId: e.callId,\n toolName: e.toolCall.toolName,\n toolCallId: e.toolCall.toolCallId,\n input: e.toolCall.input,\n });\n },\n onToolExecutionEnd(event) {\n const e = event as {\n callId: string;\n toolCall: { toolCallId: string };\n toolOutput: { type: string; output?: unknown; error?: unknown };\n };\n const isError = e.toolOutput.type === 'error';\n if (isError) bucketFor(e.callId).errored = true;\n record(e.callId, {\n ts: Date.now(),\n kind: 'tool-end',\n callId: e.callId,\n toolCallId: e.toolCall.toolCallId,\n isError,\n output: isError ? e.toolOutput.error : e.toolOutput.output,\n });\n },\n onStepFinish(event) {\n const e = event as {\n callId: string;\n usage?: unknown;\n content?: unknown[];\n recordOutputs?: boolean;\n };\n record(e.callId, {\n ts: Date.now(),\n kind: 'step-finish',\n callId: e.callId,\n usage: e.usage,\n // The model's output content, unless `recordOutputs: false`.\n ...(e.recordOutputs === false || e.content == null\n ? {}\n : { output: e.content }),\n });\n },\n onEnd(event) {\n const e = event as {\n callId: string;\n finishReason?: unknown;\n usage?: unknown;\n totalUsage?: unknown;\n };\n record(e.callId, {\n ts: Date.now(),\n kind: 'turn-finish',\n callId: e.callId,\n finishReason: e.finishReason,\n usage: e.totalUsage ?? e.usage,\n });\n finishTurn(e.callId);\n },\n onError(error) {\n if (lastOpenCallId != null) bucketFor(lastOpenCallId).errored = true;\n record(lastOpenCallId, {\n ts: Date.now(),\n kind: 'error',\n error:\n error instanceof Error\n ? { name: error.name, message: error.message }\n : error,\n });\n },\n ingestDiagnostic(diagnostic: HarnessDiagnostic) {\n if (diagnostic.level === 'error' && lastOpenCallId != null) {\n bucketFor(lastOpenCallId).errored = true;\n }\n record(lastOpenCallId, {\n ts: diagnostic.timestamp,\n kind: 'diagnostic',\n diagnostic,\n });\n },\n };\n}\n","import type { Telemetry } from 'ai';\n\n/**\n * A harness observability reporter that renders an ASCII trace tree of a\n * turn's span lifecycle (turn → steps → tools) to a stream at turn end. It is a\n * `Telemetry` integration — register it in `telemetry.integrations`. Useful for\n * zero-setup local debugging when no OTel collector is wired up; a real OTel\n * backend (via `@ai-sdk/otel`) is a strict superset.\n */\nexport interface TraceTreeReporterOptions {\n /** Where to write the rendered tree. Default `process.stderr.write`. */\n write?: (chunk: string) => void;\n}\n\ntype Node = {\n label: string;\n startMs: number;\n endMs?: number;\n children: Node[];\n};\n\nexport function createTraceTreeReporter(\n options: TraceTreeReporterOptions = {},\n): Telemetry {\n const write =\n options.write ?? ((chunk: string) => void process.stderr.write(chunk));\n\n type TurnState = {\n root: Node;\n step?: Node;\n tools: Map<string, Node>;\n };\n const turns = new Map<string, TurnState>();\n\n const render = (node: Node, depth: number): string => {\n const indent = ' '.repeat(depth);\n const dur =\n node.endMs != null\n ? `${Math.max(0, Math.round(node.endMs - node.startMs))}ms`\n : '(open)';\n let out = `${indent}- ${node.label} ${dur}\\n`;\n for (const child of node.children) out += render(child, depth + 1);\n return out;\n };\n\n return {\n onStart(event) {\n const e = event as {\n callId: string;\n operationId?: string;\n modelId?: string;\n };\n turns.set(e.callId, {\n root: {\n label: `${e.operationId ?? 'turn'}${e.modelId ? ` ${e.modelId}` : ''}`,\n startMs: Date.now(),\n children: [],\n },\n tools: new Map(),\n });\n },\n onStepStart(event) {\n const e = event as { callId: string; stepNumber?: number };\n const turn = turns.get(e.callId);\n if (!turn) return;\n const step: Node = {\n label: `step ${(e.stepNumber ?? turn.root.children.length) + 1}`,\n startMs: Date.now(),\n children: [],\n };\n turn.step = step;\n turn.root.children.push(step);\n },\n onToolExecutionStart(event) {\n const e = event as {\n callId: string;\n toolCall: { toolName: string; toolCallId: string };\n };\n const turn = turns.get(e.callId);\n const parent = turn?.step ?? turn?.root;\n if (!turn || !parent) return;\n const node: Node = {\n label: `tool ${e.toolCall.toolName}`,\n startMs: Date.now(),\n children: [],\n };\n turn.tools.set(e.toolCall.toolCallId, node);\n parent.children.push(node);\n },\n onToolExecutionEnd(event) {\n const e = event as {\n callId: string;\n toolCall: { toolCallId: string };\n toolOutput: { type: string };\n };\n const node = turns.get(e.callId)?.tools.get(e.toolCall.toolCallId);\n if (!node) return;\n node.endMs = Date.now();\n if (e.toolOutput.type === 'error') node.label += ' [error]';\n },\n onStepFinish(event) {\n const e = event as { callId: string };\n const turn = turns.get(e.callId);\n if (turn?.step) {\n turn.step.endMs = Date.now();\n turn.step = undefined;\n }\n },\n onEnd(event) {\n const e = event as { callId: string };\n const turn = turns.get(e.callId);\n if (!turn) return;\n turn.root.endMs = Date.now();\n turns.delete(e.callId);\n try {\n write(`\\n${render(turn.root, 0)}`);\n } catch {\n // Never let rendering break the turn.\n }\n },\n };\n}\n"],"mappings":"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","names":["_a","_a","e"]}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { FlexibleSchema } from '@ai-sdk/provider-utils';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Diagnostic event surfaced by {@link SandboxChannel} during its connection
|
|
6
|
+
* lifecycle. Silent unless a consumer wires `onDebug`. Reconnects are otherwise
|
|
7
|
+
* invisible — the channel reconnects transparently and the in-flight turn keeps
|
|
8
|
+
* streaming.
|
|
9
|
+
*/
|
|
10
|
+
type SandboxChannelDebugEvent = {
|
|
11
|
+
event: 'reconnect-attempt';
|
|
12
|
+
attempt: number;
|
|
13
|
+
lastSeenEventId: number;
|
|
14
|
+
} | {
|
|
15
|
+
event: 'reconnected';
|
|
16
|
+
attempt: number;
|
|
17
|
+
lastSeenEventId: number;
|
|
18
|
+
} | {
|
|
19
|
+
event: 'reconnect-failed';
|
|
20
|
+
attempts: number;
|
|
21
|
+
lastSeenEventId: number;
|
|
22
|
+
cause: unknown;
|
|
23
|
+
};
|
|
24
|
+
interface SandboxChannelReconnectOptions {
|
|
25
|
+
/** Give up reconnecting after this many milliseconds. Default 30_000. */
|
|
26
|
+
readonly maxElapsedMs?: number;
|
|
27
|
+
/** First backoff delay. Default 50. */
|
|
28
|
+
readonly initialDelayMs?: number;
|
|
29
|
+
/** Backoff ceiling. Default 2_000. */
|
|
30
|
+
readonly maxDelayMs?: number;
|
|
31
|
+
}
|
|
32
|
+
interface SandboxChannelOptions<TOut> {
|
|
33
|
+
/**
|
|
34
|
+
* Open a fresh WebSocket to the bridge and resolve once it is ready to carry
|
|
35
|
+
* frames (i.e. after any adapter-specific handshake such as Claude Code's
|
|
36
|
+
* `bridge-hello`). Called once by {@link SandboxChannel.open} and again on
|
|
37
|
+
* every transient reconnect. Must reject if the connection cannot be
|
|
38
|
+
* established.
|
|
39
|
+
*/
|
|
40
|
+
connect: () => Promise<WebSocket>;
|
|
41
|
+
/** Schema validating inbound (bridge → host) frames. */
|
|
42
|
+
outboundSchema: FlexibleSchema<TOut>;
|
|
43
|
+
reconnect?: SandboxChannelReconnectOptions;
|
|
44
|
+
onDebug?: (event: SandboxChannelDebugEvent) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Sink for forwarded bridge diagnostics — `sandbox-log` (captured
|
|
47
|
+
* console lines) and `debug-event` (structured) frames. When set, these
|
|
48
|
+
* frame types are routed here instead of the per-type listener dispatch, so
|
|
49
|
+
* they never reach the consumer's stream. Typed to the diagnostic members of
|
|
50
|
+
* `TOut`, so it is a no-op union for channels whose protocol has none.
|
|
51
|
+
*/
|
|
52
|
+
onDiagnostic?: (event: Extract<TOut, {
|
|
53
|
+
type: 'sandbox-log' | 'debug-event';
|
|
54
|
+
}>) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Seed the host-side cursor before the first connect. Pass the
|
|
57
|
+
* `lastSeenEventId` persisted from a prior process so the bridge replays only
|
|
58
|
+
* events past it when this channel opens with `{ resume: true }` — the
|
|
59
|
+
* cross-process attach handshake. Defaults to `0` (fresh session).
|
|
60
|
+
*/
|
|
61
|
+
initialLastSeenEventId?: number;
|
|
62
|
+
}
|
|
63
|
+
type EventTypeOf<TOut extends {
|
|
64
|
+
type: string;
|
|
65
|
+
}> = TOut['type'];
|
|
66
|
+
type Listener<TOut extends {
|
|
67
|
+
type: string;
|
|
68
|
+
}, T extends EventTypeOf<TOut>> = (event: Extract<TOut, {
|
|
69
|
+
type: T;
|
|
70
|
+
}>) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Host-side typed wrapper around the bridge WebSocket connection.
|
|
73
|
+
*
|
|
74
|
+
* Buffers inbound messages until a listener for their type is registered, so
|
|
75
|
+
* callers that subscribe asynchronously do not miss early frames. Inbound
|
|
76
|
+
* dispatch is serialised through a promise chain so a `close` event that
|
|
77
|
+
* arrives on the same microtask as the final `finish` message does not fire
|
|
78
|
+
* close handlers until the message has been dispatched.
|
|
79
|
+
*
|
|
80
|
+
* Survives transient disconnects. The bridge keeps running and
|
|
81
|
+
* accumulates events in an in-memory log keyed by a monotonic `seq`; on an
|
|
82
|
+
* unexpected socket drop this channel re-invokes `connect`, re-wires the new
|
|
83
|
+
* socket, and asks the bridge to replay everything past `lastSeenEventId`. The
|
|
84
|
+
* in-flight turn never observes the blip — `onClose` fires only after a
|
|
85
|
+
* host-initiated close or once the reconnect budget is exhausted.
|
|
86
|
+
*/
|
|
87
|
+
declare class SandboxChannel<TOut extends {
|
|
88
|
+
type: string;
|
|
89
|
+
}, TIn extends {
|
|
90
|
+
type: string;
|
|
91
|
+
} = {
|
|
92
|
+
type: string;
|
|
93
|
+
}> {
|
|
94
|
+
private readonly listeners;
|
|
95
|
+
private readonly buffered;
|
|
96
|
+
private readonly onCloseHandlers;
|
|
97
|
+
private readonly connectThunk;
|
|
98
|
+
private readonly outboundSchema;
|
|
99
|
+
private readonly onDebug;
|
|
100
|
+
private readonly onDiagnostic;
|
|
101
|
+
private readonly maxElapsedMs;
|
|
102
|
+
private readonly initialDelayMs;
|
|
103
|
+
private readonly maxDelayMs;
|
|
104
|
+
private ws;
|
|
105
|
+
private connected;
|
|
106
|
+
/** Host has begun teardown; suppresses reconnect so a bridge-side close finalises. */
|
|
107
|
+
private closing;
|
|
108
|
+
/**
|
|
109
|
+
* Host has gracefully suspended (slice boundary). Inbound frames are ignored
|
|
110
|
+
* from this point so the cursor stops advancing exactly at the last delivered
|
|
111
|
+
* event — the bridge keeps the turn running and the not-yet-delivered tail is
|
|
112
|
+
* replayed to the next process on `resume`.
|
|
113
|
+
*/
|
|
114
|
+
private suspended;
|
|
115
|
+
/** Channel is fully torn down; `send` throws and `onClose` has fired. */
|
|
116
|
+
private terminal;
|
|
117
|
+
private _lastSeenEventId;
|
|
118
|
+
private readonly pendingSends;
|
|
119
|
+
private dispatchChain;
|
|
120
|
+
constructor(options: SandboxChannelOptions<TOut>);
|
|
121
|
+
/**
|
|
122
|
+
* Highest bridge event `seq` this channel has observed. Persist it (e.g. via
|
|
123
|
+
* the adapter's resume handle) so a future process can seed
|
|
124
|
+
* {@link SandboxChannelOptions.initialLastSeenEventId} and attach.
|
|
125
|
+
*/
|
|
126
|
+
get lastSeenEventId(): number;
|
|
127
|
+
/**
|
|
128
|
+
* Establish the initial connection. A single attempt — startup failures
|
|
129
|
+
* reject so the caller can fail `doStart` cleanly. Reconnect retries apply
|
|
130
|
+
* only to drops after a successful open.
|
|
131
|
+
*
|
|
132
|
+
* Pass `{ resume: true }` to attach to a bridge that is already mid-session:
|
|
133
|
+
* after the socket opens, the channel sends `{ type: 'resume', lastSeenEventId }`
|
|
134
|
+
* so the bridge replays everything past the seeded cursor. This is the
|
|
135
|
+
* cross-process attach handshake — identical to what a transient reconnect
|
|
136
|
+
* does, but triggered by the initial open from a new process.
|
|
137
|
+
*/
|
|
138
|
+
open(opts?: {
|
|
139
|
+
resume?: boolean;
|
|
140
|
+
}): Promise<void>;
|
|
141
|
+
on<T extends EventTypeOf<TOut>>(type: T, listener: Listener<TOut, T>): () => void;
|
|
142
|
+
onClose(handler: (code: number, reason: string) => void): void;
|
|
143
|
+
send(message: TIn): void;
|
|
144
|
+
/**
|
|
145
|
+
* Mark that the host is tearing the session down. The next socket close is
|
|
146
|
+
* then treated as terminal rather than triggering a reconnect. Call before
|
|
147
|
+
* sending a `shutdown` / `detach` message whose ack the bridge follows with a
|
|
148
|
+
* socket close.
|
|
149
|
+
*/
|
|
150
|
+
beginClose(): void;
|
|
151
|
+
close(): void;
|
|
152
|
+
/**
|
|
153
|
+
* Gracefully suspend at a slice boundary: stop processing inbound frames
|
|
154
|
+
* (so the cursor freezes at the last delivered event), drain any frames
|
|
155
|
+
* already queued for dispatch, then close the socket and finalise with the
|
|
156
|
+
* close reason `'suspended'`. Resolves with the final `lastSeenEventId`.
|
|
157
|
+
*
|
|
158
|
+
* The bridge keeps the in-flight turn running (a host socket close never
|
|
159
|
+
* aborts it) and accumulates events past the cursor for the next process to
|
|
160
|
+
* `resume`. Unlike {@link close}, the consumer's active turn is wound down
|
|
161
|
+
* cleanly — adapters distinguish a suspend from an unexpected drop via the
|
|
162
|
+
* `'suspended'` close reason and resolve `done` successfully.
|
|
163
|
+
*/
|
|
164
|
+
suspend(): Promise<number>;
|
|
165
|
+
isClosed(): boolean;
|
|
166
|
+
private wire;
|
|
167
|
+
private reconnectLoop;
|
|
168
|
+
private rawSend;
|
|
169
|
+
private flushPending;
|
|
170
|
+
private enqueue;
|
|
171
|
+
private handleIncoming;
|
|
172
|
+
private dispatch;
|
|
173
|
+
private finalizeClose;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Recovery rung selected from an on-disk bridge event log when attach is not
|
|
178
|
+
* possible (the bridge process is gone): `'replay'` when the log holds a
|
|
179
|
+
* complete turn the host can resume from its cursor, `'rerun'` otherwise.
|
|
180
|
+
*/
|
|
181
|
+
type DiskLogRecoveryMode = 'replay' | 'rerun';
|
|
182
|
+
/**
|
|
183
|
+
* Decide whether a respawned bridge can `replay` a turn from its persisted
|
|
184
|
+
* `event-log.ndjson`, or must `rerun` it from scratch.
|
|
185
|
+
*
|
|
186
|
+
* A turn is replayable only when its log ends in a terminal `finish` event —
|
|
187
|
+
* a log that is missing, empty, or ends mid-turn means the bridge died before
|
|
188
|
+
* completing the turn, so there is no coherent tail to deliver and the runtime
|
|
189
|
+
* must re-run it (continuing its own thread from the sandbox snapshot).
|
|
190
|
+
*
|
|
191
|
+
* @param eventLog Raw contents of `event-log.ndjson` (newline-delimited JSON),
|
|
192
|
+
* or `null`/`undefined`/empty when the file is absent.
|
|
193
|
+
*/
|
|
194
|
+
declare function classifyDiskLog(eventLog: string | null | undefined): Promise<DiskLogRecoveryMode>;
|
|
195
|
+
|
|
196
|
+
export { type DiskLogRecoveryMode, SandboxChannel, type SandboxChannelDebugEvent, type SandboxChannelOptions, type SandboxChannelReconnectOptions, classifyDiskLog };
|