@ai-sdk/harness 0.0.0-6b196531-20260710185421

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