@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.
- package/CHANGELOG.md +414 -0
- package/LICENSE +13 -0
- package/README.md +176 -0
- package/agent/index.ts +56 -0
- package/bridge/index.ts +10 -0
- package/dist/agent/index.d.ts +1631 -0
- package/dist/agent/index.js +3491 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/bridge/index.d.ts +129 -0
- package/dist/bridge/index.js +482 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/index.d.ts +1587 -0
- package/dist/index.js +517 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.d.ts +329 -0
- package/dist/utils/index.js +1241 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +100 -0
- package/src/agent/harness-agent-session.ts +518 -0
- package/src/agent/harness-agent-settings.ts +187 -0
- package/src/agent/harness-agent-tool-approval-continuation.ts +94 -0
- package/src/agent/harness-agent-tool-types.ts +15 -0
- package/src/agent/harness-agent-types.ts +50 -0
- package/src/agent/harness-agent.ts +865 -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 +731 -0
- package/src/agent/internal/lifecycle-state-validation.ts +95 -0
- package/src/agent/internal/permission-mode.ts +50 -0
- package/src/agent/internal/resolve-observability.ts +128 -0
- package/src/agent/internal/run-prompt.ts +901 -0
- package/src/agent/internal/sandbox-bootstrap.ts +266 -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/tool-filtering.ts +114 -0
- package/src/agent/internal/translate-stream-part.ts +221 -0
- package/src/agent/internal/turn-telemetry.ts +361 -0
- package/src/agent/observability/file-reporter.ts +206 -0
- package/src/agent/observability/index.ts +15 -0
- package/src/agent/observability/trace-tree-reporter.ts +122 -0
- package/src/agent/observability/types.ts +86 -0
- package/src/agent/prepare-harness-sandbox-template.ts +68 -0
- package/src/agent/prepare-sandbox-for-harness.ts +165 -0
- package/src/bridge/index.ts +797 -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/utils/ai-gateway-auth.ts +15 -0
- package/src/utils/bridge-diagnostics.ts +213 -0
- package/src/utils/bridge-ready.ts +277 -0
- package/src/utils/classify-disk-log.ts +43 -0
- package/src/utils/index.ts +31 -0
- package/src/utils/sandbox-channel.ts +525 -0
- package/src/utils/sandbox-home-dir.ts +22 -0
- package/src/utils/shell-quote.ts +3 -0
- package/src/utils/write-skills.ts +141 -0
- package/src/v1/harness-v1-bootstrap.ts +46 -0
- package/src/v1/harness-v1-bridge-protocol.ts +342 -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-lifecycle-state.ts +65 -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-sandbox-provider.ts +76 -0
- package/src/v1/harness-v1-session.ts +280 -0
- package/src/v1/harness-v1-skill.ts +36 -0
- package/src/v1/harness-v1-stream-part.ts +363 -0
- package/src/v1/harness-v1-tool-filtering.ts +25 -0
- package/src/v1/harness-v1-tool-spec.ts +31 -0
- package/src/v1/harness-v1.ts +94 -0
- package/src/v1/index.ts +99 -0
- package/utils/index.ts +1 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
type BridgeState = 'init' | 'waiting' | 'running' | 'draining' | 'done';
|
|
2
|
+
/** Outbound turn event the adapter emits. `seq` is added by the runtime. */
|
|
3
|
+
type BridgeEvent = Record<string, unknown> & {
|
|
4
|
+
type: string;
|
|
5
|
+
};
|
|
6
|
+
type BridgeDebugLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
|
7
|
+
/**
|
|
8
|
+
* Per-session diagnostics config. The host resolves it from settings +
|
|
9
|
+
* env and sends it on `start.debug`; the bridge gates console capture and
|
|
10
|
+
* structured `debug-event`s on it. When disabled, nothing is captured or
|
|
11
|
+
* emitted and no `seq` is consumed.
|
|
12
|
+
*/
|
|
13
|
+
interface BridgeDebugConfig {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
level?: BridgeDebugLevel;
|
|
16
|
+
subsystems?: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Per-turn surface handed to {@link RunBridgeOptions.onStart}. The adapter
|
|
20
|
+
* drives its runtime against these primitives; the runtime owns the transport.
|
|
21
|
+
*/
|
|
22
|
+
interface BridgeTurn {
|
|
23
|
+
/**
|
|
24
|
+
* Emit a turn event to the host. Stamps a monotonic `seq`, appends to the
|
|
25
|
+
* in-memory replay log, and sends to the live socket (best-effort — if the
|
|
26
|
+
* host is mid-reconnect the event waits in the log and is replayed on
|
|
27
|
+
* resume).
|
|
28
|
+
*/
|
|
29
|
+
emit(event: BridgeEvent): void;
|
|
30
|
+
/**
|
|
31
|
+
* Register interest in a host-executed tool result and resolve when the
|
|
32
|
+
* matching `tool-result` arrives. The adapter emits the `tool-call` event
|
|
33
|
+
* itself (via {@link emit}) using the same `toolCallId`.
|
|
34
|
+
*/
|
|
35
|
+
requestToolResult(toolCallId: string): Promise<{
|
|
36
|
+
output: unknown;
|
|
37
|
+
isError?: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Register interest in a host approval decision and resolve when the matching
|
|
41
|
+
* `tool-approval-response` arrives. The adapter emits the
|
|
42
|
+
* `tool-approval-request` event itself using the same `approvalId`.
|
|
43
|
+
*/
|
|
44
|
+
requestToolApproval(approvalId: string): Promise<{
|
|
45
|
+
approved: boolean;
|
|
46
|
+
reason?: string;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Live queue of mid-turn user messages. The runtime pushes inbound
|
|
50
|
+
* `user-message` text here; the adapter drains it as its runtime accepts
|
|
51
|
+
* interactive input.
|
|
52
|
+
*/
|
|
53
|
+
readonly pendingUserMessages: string[];
|
|
54
|
+
/** Aborts when the host sends `abort`. */
|
|
55
|
+
readonly abortSignal: AbortSignal;
|
|
56
|
+
/**
|
|
57
|
+
* Register the runtime-specific interrupt hook for this active turn. The
|
|
58
|
+
* shared bridge invokes it when the host sends `interrupt`, then acknowledges
|
|
59
|
+
* only after the hook settles.
|
|
60
|
+
*/
|
|
61
|
+
onInterrupt(handler: () => void | Promise<void>): void;
|
|
62
|
+
/** True for the first turn since this bridge process started. */
|
|
63
|
+
readonly firstTurn: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Emit a structured diagnostic. Gated by the session's debug level +
|
|
66
|
+
* subsystem filter; a no-op when diagnostics are disabled. Adapters use this
|
|
67
|
+
* for runtime-level instrumentation; raw `console.*` output is captured and
|
|
68
|
+
* forwarded automatically.
|
|
69
|
+
*/
|
|
70
|
+
bridgeLog(input: {
|
|
71
|
+
level?: BridgeDebugLevel;
|
|
72
|
+
subsystem: string;
|
|
73
|
+
message: string;
|
|
74
|
+
attrs?: Record<string, unknown>;
|
|
75
|
+
error?: unknown;
|
|
76
|
+
}): void;
|
|
77
|
+
/**
|
|
78
|
+
* Emit a non-fatal bridge warning to stderr using the runtime's harness
|
|
79
|
+
* prefix. This is diagnostic-only: it does not emit a stream event, does not
|
|
80
|
+
* consume a `seq`, and does not fail the turn.
|
|
81
|
+
*/
|
|
82
|
+
emitWarning(input: {
|
|
83
|
+
message: string;
|
|
84
|
+
}): void;
|
|
85
|
+
emitError(input: {
|
|
86
|
+
error: unknown;
|
|
87
|
+
message?: string;
|
|
88
|
+
}): void;
|
|
89
|
+
}
|
|
90
|
+
interface RunBridgeOptions<TStart extends {
|
|
91
|
+
type: 'start';
|
|
92
|
+
}> {
|
|
93
|
+
/** Identifier written into `bridge-meta.json` (`'claude-code'` / `'codex'`). */
|
|
94
|
+
bridgeType: string;
|
|
95
|
+
/** Directory for `bridge-meta.json` / `start-config.json`. Created if absent. */
|
|
96
|
+
bridgeStateDir: string;
|
|
97
|
+
/** Drive one prompt turn. Rejections surface to the host as an `error` event. */
|
|
98
|
+
onStart(start: TStart, turn: BridgeTurn): Promise<void>;
|
|
99
|
+
/** Produce the adapter-defined resume payload for a `detach`. Defaults to `{}`. */
|
|
100
|
+
onDetach?(): unknown | Promise<unknown>;
|
|
101
|
+
/** WS port. Defaults to `BRIDGE_WS_PORT` env (0 = OS-assigned). */
|
|
102
|
+
port?: number;
|
|
103
|
+
/** Auth token. Defaults to `BRIDGE_CHANNEL_TOKEN` env. */
|
|
104
|
+
token?: string;
|
|
105
|
+
/** Called with the bound port once the server is listening. */
|
|
106
|
+
onListening?(port: number): void;
|
|
107
|
+
/**
|
|
108
|
+
* Tear the process down after a `shutdown` / `detach`. Defaults to closing
|
|
109
|
+
* the server and calling `process.exit(0)`. Overridable for tests.
|
|
110
|
+
*/
|
|
111
|
+
onExit?(): void;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Boot the bridge: bind the WebSocket server, announce `bridge-ready`, and
|
|
115
|
+
* service host connections for the lifetime of the process. Resolves once the
|
|
116
|
+
* server is listening; the process then stays alive on the server until a
|
|
117
|
+
* `shutdown` / `detach` exits it.
|
|
118
|
+
*/
|
|
119
|
+
interface BridgeHandle {
|
|
120
|
+
/** The port the WebSocket server bound to. */
|
|
121
|
+
readonly port: number;
|
|
122
|
+
/** Close the WebSocket server. Does not call `process.exit`. */
|
|
123
|
+
close(): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
declare function runBridge<TStart extends {
|
|
126
|
+
type: 'start';
|
|
127
|
+
}>(options: RunBridgeOptions<TStart>): Promise<BridgeHandle>;
|
|
128
|
+
|
|
129
|
+
export { type BridgeDebugConfig, type BridgeDebugLevel, type BridgeEvent, type BridgeHandle, type BridgeState, type BridgeTurn, type RunBridgeOptions, runBridge };
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
// src/bridge/index.ts
|
|
2
|
+
import { appendFile, mkdir, writeFile } from "fs/promises";
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { env as procEnv, pid, stdout } from "process";
|
|
5
|
+
import { WebSocketServer } from "ws";
|
|
6
|
+
var DEBUG_LEVEL_WEIGHT = {
|
|
7
|
+
error: 0,
|
|
8
|
+
warn: 1,
|
|
9
|
+
info: 2,
|
|
10
|
+
debug: 3,
|
|
11
|
+
trace: 4
|
|
12
|
+
};
|
|
13
|
+
function subsystemMatches(filters, subsystem) {
|
|
14
|
+
if (!filters || filters.length === 0) return true;
|
|
15
|
+
return filters.some(
|
|
16
|
+
(filter) => subsystem === filter || subsystem.startsWith(`${filter}.`)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function formatBridgeError(err) {
|
|
20
|
+
if (err instanceof Error) {
|
|
21
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
22
|
+
}
|
|
23
|
+
if (typeof err === "string") {
|
|
24
|
+
return { message: err };
|
|
25
|
+
}
|
|
26
|
+
if (err !== null && typeof err === "object") {
|
|
27
|
+
try {
|
|
28
|
+
return { message: JSON.stringify(err) };
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { message: String(err) };
|
|
33
|
+
}
|
|
34
|
+
function parseEnvList(value) {
|
|
35
|
+
if (!value) return void 0;
|
|
36
|
+
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
37
|
+
return items.length > 0 ? items : void 0;
|
|
38
|
+
}
|
|
39
|
+
var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
40
|
+
var WS_OPEN = 1;
|
|
41
|
+
async function runBridge(options) {
|
|
42
|
+
const { bridgeType, bridgeStateDir, onStart, onDetach } = options;
|
|
43
|
+
const expectedToken = options.token ?? procEnv.BRIDGE_CHANNEL_TOKEN ?? "";
|
|
44
|
+
const bridgeWsPort = options.port ?? parseInt(procEnv.BRIDGE_WS_PORT ?? "0", 10);
|
|
45
|
+
const bridgeMetaPath = `${bridgeStateDir}/bridge-meta.json`;
|
|
46
|
+
const startConfigPath = `${bridgeStateDir}/start-config.json`;
|
|
47
|
+
const rerunStartConfigPath = `${bridgeStateDir}/rerun-start-config.json`;
|
|
48
|
+
const eventLogPath = `${bridgeStateDir}/event-log.ndjson`;
|
|
49
|
+
try {
|
|
50
|
+
await mkdir(bridgeStateDir, { recursive: true });
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
let currentBoundPort = 0;
|
|
54
|
+
let currentTurnState = "init";
|
|
55
|
+
let activeSocket;
|
|
56
|
+
let isFirstTurn = true;
|
|
57
|
+
let turnAbort;
|
|
58
|
+
let currentUserMessages;
|
|
59
|
+
let currentInterruptHandler;
|
|
60
|
+
let debugConfig;
|
|
61
|
+
let consoleCaptureInstalled = false;
|
|
62
|
+
const envDebugEnabled = ENV_TRUTHY.has(
|
|
63
|
+
(procEnv.HARNESS_DEBUG ?? "").toLowerCase()
|
|
64
|
+
);
|
|
65
|
+
let seqCounter = 0;
|
|
66
|
+
let eventLog = [];
|
|
67
|
+
let diskBuffer = "";
|
|
68
|
+
let flushPromise = null;
|
|
69
|
+
const flushEventsToDisk = async () => {
|
|
70
|
+
while (diskBuffer.length > 0) {
|
|
71
|
+
const buf = diskBuffer;
|
|
72
|
+
diskBuffer = "";
|
|
73
|
+
await appendFile(eventLogPath, buf).catch(() => {
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const scheduleEventFlush = () => {
|
|
78
|
+
if (flushPromise) return;
|
|
79
|
+
flushPromise = new Promise((resolve) => {
|
|
80
|
+
setImmediate(() => {
|
|
81
|
+
void flushEventsToDisk().finally(resolve);
|
|
82
|
+
});
|
|
83
|
+
}).finally(() => {
|
|
84
|
+
flushPromise = null;
|
|
85
|
+
if (diskBuffer.length > 0) {
|
|
86
|
+
scheduleEventFlush();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
const flushPendingEventsToDisk = async () => {
|
|
91
|
+
if (diskBuffer.length > 0 && !flushPromise) {
|
|
92
|
+
scheduleEventFlush();
|
|
93
|
+
}
|
|
94
|
+
let inFlight = flushPromise;
|
|
95
|
+
while (inFlight) {
|
|
96
|
+
await inFlight;
|
|
97
|
+
inFlight = flushPromise;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const replayFromDisk = procEnv.BRIDGE_REPLAY_FROM_DISK === "1";
|
|
101
|
+
if (replayFromDisk && existsSync(eventLogPath)) {
|
|
102
|
+
try {
|
|
103
|
+
const lines = readFileSync(eventLogPath, "utf8").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
104
|
+
eventLog = lines.map((line) => ({
|
|
105
|
+
seq: JSON.parse(line).seq,
|
|
106
|
+
line
|
|
107
|
+
}));
|
|
108
|
+
seqCounter = eventLog.at(-1)?.seq ?? 0;
|
|
109
|
+
} catch {
|
|
110
|
+
eventLog = [];
|
|
111
|
+
seqCounter = 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const pendingToolResults = /* @__PURE__ */ new Map();
|
|
115
|
+
const pendingToolApprovals = /* @__PURE__ */ new Map();
|
|
116
|
+
const writeBridgeMeta = async (state) => {
|
|
117
|
+
try {
|
|
118
|
+
await writeFile(
|
|
119
|
+
bridgeMetaPath,
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
type: bridgeType,
|
|
122
|
+
port: currentBoundPort,
|
|
123
|
+
state,
|
|
124
|
+
pid
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const writeStartConfig = async (start) => {
|
|
131
|
+
try {
|
|
132
|
+
const serialized = JSON.stringify(start);
|
|
133
|
+
await writeFile(startConfigPath, serialized);
|
|
134
|
+
if (!existsSync(rerunStartConfigPath)) {
|
|
135
|
+
await writeFile(rerunStartConfigPath, serialized);
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const sendControl = (msg) => {
|
|
141
|
+
if (activeSocket?.readyState === WS_OPEN) {
|
|
142
|
+
try {
|
|
143
|
+
activeSocket.send(JSON.stringify(msg));
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const emit = (event) => {
|
|
149
|
+
const seq = ++seqCounter;
|
|
150
|
+
const line = JSON.stringify({ ...event, seq });
|
|
151
|
+
eventLog.push({ seq, line });
|
|
152
|
+
diskBuffer += `${line}
|
|
153
|
+
`;
|
|
154
|
+
scheduleEventFlush();
|
|
155
|
+
if (activeSocket?.readyState === WS_OPEN) {
|
|
156
|
+
try {
|
|
157
|
+
activeSocket.send(line);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const replay = (ws, afterSeq) => {
|
|
163
|
+
for (const entry of eventLog) {
|
|
164
|
+
if (entry.seq > afterSeq && ws.readyState === WS_OPEN) {
|
|
165
|
+
ws.send(entry.line);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const shouldEmitDebugEvent = (level, subsystem) => {
|
|
170
|
+
if (!debugConfig?.enabled) return false;
|
|
171
|
+
const threshold = debugConfig.level ?? "debug";
|
|
172
|
+
if (DEBUG_LEVEL_WEIGHT[level] > DEBUG_LEVEL_WEIGHT[threshold]) return false;
|
|
173
|
+
return subsystemMatches(debugConfig.subsystems, subsystem);
|
|
174
|
+
};
|
|
175
|
+
const rawStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
176
|
+
const rawStderrWrite = process.stderr.write.bind(process.stderr);
|
|
177
|
+
const writeErrorToStderr = (input) => {
|
|
178
|
+
try {
|
|
179
|
+
const formatted = formatBridgeError(input.error);
|
|
180
|
+
rawStderrWrite(
|
|
181
|
+
`[harness:${bridgeType}:error] ${input.message}: ${formatted.message}
|
|
182
|
+
`
|
|
183
|
+
);
|
|
184
|
+
if (formatted.stack) {
|
|
185
|
+
rawStderrWrite(`${formatted.stack}
|
|
186
|
+
`);
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const emitWarning = (input) => {
|
|
192
|
+
try {
|
|
193
|
+
for (const line of input.message.split("\n")) {
|
|
194
|
+
if (line.trim().length > 0) {
|
|
195
|
+
rawStderrWrite(`[harness:${bridgeType}:warn] ${line}
|
|
196
|
+
`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const emitError = (input) => {
|
|
203
|
+
writeErrorToStderr({
|
|
204
|
+
message: input.message ?? "bridge error",
|
|
205
|
+
error: input.error
|
|
206
|
+
});
|
|
207
|
+
emit({ type: "error", error: serialiseError(input.error) });
|
|
208
|
+
};
|
|
209
|
+
const installConsoleCapture = () => {
|
|
210
|
+
if (consoleCaptureInstalled) return;
|
|
211
|
+
consoleCaptureInstalled = true;
|
|
212
|
+
const buffers = {
|
|
213
|
+
stdout: "",
|
|
214
|
+
stderr: ""
|
|
215
|
+
};
|
|
216
|
+
const patch = (stream, raw) => (chunk, encoding, cb) => {
|
|
217
|
+
if (debugConfig?.enabled) {
|
|
218
|
+
try {
|
|
219
|
+
const enc = typeof encoding === "string" ? encoding : "utf8";
|
|
220
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(
|
|
221
|
+
enc
|
|
222
|
+
);
|
|
223
|
+
const combined = buffers[stream] + text.replace(/\r\n/g, "\n");
|
|
224
|
+
const parts = combined.split("\n");
|
|
225
|
+
buffers[stream] = parts.pop() ?? "";
|
|
226
|
+
for (const line of parts) {
|
|
227
|
+
const trimmed = line.replace(/\s+$/, "");
|
|
228
|
+
if (trimmed) {
|
|
229
|
+
emit({
|
|
230
|
+
type: "sandbox-log",
|
|
231
|
+
source: bridgeType,
|
|
232
|
+
stream,
|
|
233
|
+
line: trimmed
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return raw(
|
|
241
|
+
chunk,
|
|
242
|
+
encoding,
|
|
243
|
+
cb
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
process.stdout.write = patch(
|
|
247
|
+
"stdout",
|
|
248
|
+
rawStdoutWrite
|
|
249
|
+
);
|
|
250
|
+
process.stderr.write = patch(
|
|
251
|
+
"stderr",
|
|
252
|
+
rawStderrWrite
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
const handleInbound = async (msg, ws) => {
|
|
256
|
+
switch (msg.type) {
|
|
257
|
+
case "start": {
|
|
258
|
+
const firstTurn = isFirstTurn;
|
|
259
|
+
isFirstTurn = false;
|
|
260
|
+
eventLog = [];
|
|
261
|
+
diskBuffer = "";
|
|
262
|
+
void writeFile(eventLogPath, "").catch(() => {
|
|
263
|
+
});
|
|
264
|
+
turnAbort = new AbortController();
|
|
265
|
+
currentTurnState = "running";
|
|
266
|
+
currentInterruptHandler = void 0;
|
|
267
|
+
void writeStartConfig(msg);
|
|
268
|
+
void writeBridgeMeta("running");
|
|
269
|
+
const startDebug = msg.debug;
|
|
270
|
+
debugConfig = {
|
|
271
|
+
enabled: startDebug?.enabled ?? envDebugEnabled,
|
|
272
|
+
level: startDebug?.level ?? procEnv.HARNESS_DEBUG_LEVEL,
|
|
273
|
+
subsystems: startDebug?.subsystems ?? parseEnvList(procEnv.HARNESS_DEBUG_SUBSYSTEMS)
|
|
274
|
+
};
|
|
275
|
+
if (debugConfig.enabled) {
|
|
276
|
+
installConsoleCapture();
|
|
277
|
+
}
|
|
278
|
+
const turn = {
|
|
279
|
+
emit,
|
|
280
|
+
requestToolResult: (toolCallId) => new Promise((resolve) => {
|
|
281
|
+
pendingToolResults.set(toolCallId, resolve);
|
|
282
|
+
}),
|
|
283
|
+
requestToolApproval: (approvalId) => new Promise((resolve) => {
|
|
284
|
+
pendingToolApprovals.set(approvalId, resolve);
|
|
285
|
+
}),
|
|
286
|
+
pendingUserMessages: [],
|
|
287
|
+
abortSignal: turnAbort.signal,
|
|
288
|
+
onInterrupt: (handler) => {
|
|
289
|
+
currentInterruptHandler = handler;
|
|
290
|
+
},
|
|
291
|
+
firstTurn,
|
|
292
|
+
bridgeLog: (input) => {
|
|
293
|
+
const level = input.level ?? "debug";
|
|
294
|
+
if (!shouldEmitDebugEvent(level, input.subsystem)) return;
|
|
295
|
+
emit({
|
|
296
|
+
type: "debug-event",
|
|
297
|
+
level,
|
|
298
|
+
subsystem: input.subsystem,
|
|
299
|
+
message: input.message,
|
|
300
|
+
...input.attrs ? { attrs: input.attrs } : {},
|
|
301
|
+
...input.error !== void 0 ? { error: formatBridgeError(input.error) } : {}
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
emitWarning,
|
|
305
|
+
emitError
|
|
306
|
+
};
|
|
307
|
+
currentUserMessages = turn.pendingUserMessages;
|
|
308
|
+
try {
|
|
309
|
+
await onStart(msg, turn);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
emitError({ error: err, message: "bridge turn failed" });
|
|
312
|
+
} finally {
|
|
313
|
+
currentInterruptHandler = void 0;
|
|
314
|
+
currentTurnState = "waiting";
|
|
315
|
+
void writeBridgeMeta("waiting");
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
case "tool-result": {
|
|
320
|
+
const resolver = pendingToolResults.get(msg.toolCallId);
|
|
321
|
+
if (resolver) {
|
|
322
|
+
pendingToolResults.delete(msg.toolCallId);
|
|
323
|
+
resolver({ output: msg.output, isError: msg.isError });
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
case "tool-approval-response": {
|
|
328
|
+
const resolver = pendingToolApprovals.get(msg.approvalId);
|
|
329
|
+
if (resolver) {
|
|
330
|
+
pendingToolApprovals.delete(msg.approvalId);
|
|
331
|
+
resolver({ approved: msg.approved, reason: msg.reason });
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
case "user-message":
|
|
336
|
+
currentUserMessages?.push(msg.text);
|
|
337
|
+
return;
|
|
338
|
+
case "abort":
|
|
339
|
+
turnAbort?.abort();
|
|
340
|
+
return;
|
|
341
|
+
case "interrupt":
|
|
342
|
+
try {
|
|
343
|
+
if (pendingToolResults.size === 0 && pendingToolApprovals.size === 0) {
|
|
344
|
+
if (currentInterruptHandler) {
|
|
345
|
+
await currentInterruptHandler();
|
|
346
|
+
} else {
|
|
347
|
+
turnAbort?.abort();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
sendControl({ type: "bridge-interrupted", ok: true });
|
|
351
|
+
} catch (err) {
|
|
352
|
+
sendControl({
|
|
353
|
+
type: "bridge-interrupted",
|
|
354
|
+
ok: false,
|
|
355
|
+
error: serialiseError(err)
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
case "resume":
|
|
360
|
+
replay(ws, msg.lastSeenEventId);
|
|
361
|
+
return;
|
|
362
|
+
case "shutdown":
|
|
363
|
+
currentTurnState = "done";
|
|
364
|
+
void writeBridgeMeta("done");
|
|
365
|
+
drainThenExit(ws, 1e3, "shutdown");
|
|
366
|
+
return;
|
|
367
|
+
case "detach": {
|
|
368
|
+
currentTurnState = "done";
|
|
369
|
+
void writeBridgeMeta("done");
|
|
370
|
+
const data = await onDetach?.() ?? {};
|
|
371
|
+
sendControl({ type: "bridge-detach", data });
|
|
372
|
+
drainThenExit(ws, 1e3, "detach");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
void writeBridgeMeta("init");
|
|
378
|
+
const wss = new WebSocketServer({ port: bridgeWsPort, host: "0.0.0.0" });
|
|
379
|
+
const exit = () => {
|
|
380
|
+
if (options.onExit) {
|
|
381
|
+
options.onExit();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
wss.close(() => process.exit(0));
|
|
385
|
+
setTimeout(() => process.exit(0), 1e3).unref();
|
|
386
|
+
};
|
|
387
|
+
const drainThenExit = (ws, code, reason) => {
|
|
388
|
+
const start = Date.now();
|
|
389
|
+
const tick = () => {
|
|
390
|
+
const drained = ws.bufferedAmount === 0 || ws.readyState !== WS_OPEN;
|
|
391
|
+
if (drained || Date.now() - start >= 5e3) {
|
|
392
|
+
void flushPendingEventsToDisk().finally(() => {
|
|
393
|
+
try {
|
|
394
|
+
ws.close(code, reason);
|
|
395
|
+
} finally {
|
|
396
|
+
exit();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
setTimeout(tick, 10).unref();
|
|
402
|
+
};
|
|
403
|
+
tick();
|
|
404
|
+
};
|
|
405
|
+
wss.on("listening", () => {
|
|
406
|
+
const addr = wss.address();
|
|
407
|
+
currentBoundPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
408
|
+
currentTurnState = "waiting";
|
|
409
|
+
void writeBridgeMeta("waiting");
|
|
410
|
+
stdout.write(
|
|
411
|
+
JSON.stringify({
|
|
412
|
+
type: "bridge-ready",
|
|
413
|
+
port: currentBoundPort
|
|
414
|
+
}) + "\n"
|
|
415
|
+
);
|
|
416
|
+
options.onListening?.(currentBoundPort);
|
|
417
|
+
});
|
|
418
|
+
wss.on("connection", (ws, req) => {
|
|
419
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
420
|
+
if (url.searchParams.get("agent_bridge_token") !== expectedToken) {
|
|
421
|
+
ws.close(1008, "unauthorized");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
activeSocket = ws;
|
|
425
|
+
sendControl({
|
|
426
|
+
type: "bridge-hello",
|
|
427
|
+
state: currentTurnState,
|
|
428
|
+
lastSeq: seqCounter
|
|
429
|
+
});
|
|
430
|
+
ws.on("message", (raw) => {
|
|
431
|
+
let parsed;
|
|
432
|
+
try {
|
|
433
|
+
const text = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
|
|
434
|
+
parsed = JSON.parse(text);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
sendControl({
|
|
437
|
+
type: "error",
|
|
438
|
+
error: `protocol parse error: ${err.message}`
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
void handleInbound(parsed, ws);
|
|
443
|
+
});
|
|
444
|
+
ws.on("close", () => {
|
|
445
|
+
if (activeSocket === ws) {
|
|
446
|
+
activeSocket = void 0;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
ws.on("error", () => {
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
process.on("uncaughtException", (err) => {
|
|
453
|
+
emitError({ error: err, message: "uncaught exception" });
|
|
454
|
+
});
|
|
455
|
+
process.on("unhandledRejection", (err) => {
|
|
456
|
+
emitError({ error: err, message: "unhandled rejection" });
|
|
457
|
+
});
|
|
458
|
+
await new Promise((resolve, reject) => {
|
|
459
|
+
if (wss.address() != null) {
|
|
460
|
+
resolve();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
wss.once("listening", resolve);
|
|
464
|
+
wss.once("error", reject);
|
|
465
|
+
});
|
|
466
|
+
return {
|
|
467
|
+
port: currentBoundPort,
|
|
468
|
+
close: () => new Promise((resolve) => {
|
|
469
|
+
wss.close(() => resolve());
|
|
470
|
+
})
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function serialiseError(err) {
|
|
474
|
+
if (err instanceof Error) {
|
|
475
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
476
|
+
}
|
|
477
|
+
return err;
|
|
478
|
+
}
|
|
479
|
+
export {
|
|
480
|
+
runBridge
|
|
481
|
+
};
|
|
482
|
+
//# sourceMappingURL=index.js.map
|