@ai-sdk/harness 1.0.133 → 1.0.135

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.
@@ -1,669 +1,598 @@
1
- // src/bridge/index.ts
2
- import { appendFile, mkdir, writeFile } from "fs/promises";
3
- import { existsSync, readFileSync } from "fs";
4
- import { randomUUID } from "crypto";
5
- import { env as procEnv, pid, stdout } from "process";
1
+ import { appendFile, mkdir, writeFile } from "node:fs/promises";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { randomUUID } from "node:crypto";
4
+ import { env, pid, stdout } from "node:process";
6
5
  import { WebSocketServer } from "ws";
7
-
8
- // src/bridge/harness-bridge-capability-unsupported-error.ts
9
- var name = "AI_HarnessBridgeCapabilityUnsupportedError";
6
+ //#region src/bridge/harness-bridge-capability-unsupported-error.ts
7
+ const name = "AI_HarnessBridgeCapabilityUnsupportedError";
8
+ /**
9
+ * Signals an unsupported capability discovered inside a sandbox bridge.
10
+ * `isInstance` also recognizes the serialized error shape received by the
11
+ * host, where it can be translated to `HarnessCapabilityUnsupportedError`.
12
+ */
10
13
  var HarnessBridgeCapabilityUnsupportedError = class extends Error {
11
- constructor({
12
- message,
13
- harnessId,
14
- cause
15
- }) {
16
- super(message);
17
- Object.defineProperty(this, "name", { value: name });
18
- this.harnessId = harnessId;
19
- this.cause = cause;
20
- }
21
- static isInstance(error) {
22
- return error != null && typeof error === "object" && "name" in error && error.name === name && "message" in error && typeof error.message === "string";
23
- }
14
+ constructor({ message, harnessId, cause }) {
15
+ super(message);
16
+ Object.defineProperty(this, "name", { value: name });
17
+ this.harnessId = harnessId;
18
+ this.cause = cause;
19
+ }
20
+ static isInstance(error) {
21
+ return error != null && typeof error === "object" && "name" in error && error.name === name && "message" in error && typeof error.message === "string";
22
+ }
24
23
  };
25
-
26
- // src/bridge/index.ts
27
- var DEBUG_LEVEL_WEIGHT = {
28
- error: 0,
29
- warn: 1,
30
- info: 2,
31
- debug: 3,
32
- trace: 4
24
+ //#endregion
25
+ //#region src/bridge/index.ts
26
+ const DEBUG_LEVEL_WEIGHT = {
27
+ error: 0,
28
+ warn: 1,
29
+ info: 2,
30
+ debug: 3,
31
+ trace: 4
33
32
  };
33
+ /** Exact-or-dotted-prefix subsystem match (`'bridge'` matches `'bridge.turn'`). */
34
34
  function subsystemMatches(filters, subsystem) {
35
- if (!filters || filters.length === 0) return true;
36
- return filters.some(
37
- (filter) => subsystem === filter || subsystem.startsWith(`${filter}.`)
38
- );
35
+ if (!filters || filters.length === 0) return true;
36
+ return filters.some((filter) => subsystem === filter || subsystem.startsWith(`${filter}.`));
39
37
  }
40
38
  function formatBridgeError(err) {
41
- if (err instanceof Error) {
42
- return { name: err.name, message: err.message, stack: err.stack };
43
- }
44
- if (typeof err === "string") {
45
- return { message: err };
46
- }
47
- if (err !== null && typeof err === "object") {
48
- try {
49
- return { message: JSON.stringify(err) };
50
- } catch {
51
- }
52
- }
53
- return { message: String(err) };
39
+ if (err instanceof Error) return {
40
+ name: err.name,
41
+ message: err.message,
42
+ stack: err.stack
43
+ };
44
+ if (typeof err === "string") return { message: err };
45
+ if (err !== null && typeof err === "object") try {
46
+ return { message: JSON.stringify(err) };
47
+ } catch {}
48
+ return { message: String(err) };
54
49
  }
55
50
  function createBridgeUserMessageQueue(options) {
56
- const messages = [];
57
- const waiters = [];
58
- const entries = /* @__PURE__ */ new Map();
59
- let closed = false;
60
- let pendingCount = 0;
61
- const enqueue = (input) => {
62
- const existing = entries.get(input.messageId);
63
- if (existing != null) {
64
- if (existing.response != null) {
65
- options.respond(existing.response);
66
- }
67
- return;
68
- }
69
- let settled = false;
70
- const settle = (response) => {
71
- if (settled) return;
72
- settled = true;
73
- pendingCount--;
74
- const entry = entries.get(input.messageId);
75
- if (entry != null) entry.response = response;
76
- options.respond(response);
77
- };
78
- const message = {
79
- messageId: input.messageId,
80
- text: input.text,
81
- accept: () => {
82
- settle({
83
- type: "user-message-response",
84
- messageId: input.messageId,
85
- accepted: true
86
- });
87
- },
88
- reject: (error) => {
89
- settle({
90
- type: "user-message-response",
91
- messageId: input.messageId,
92
- accepted: false,
93
- error: { message: formatBridgeError(error).message }
94
- });
95
- }
96
- };
97
- entries.set(input.messageId, {
98
- reject: message.reject
99
- });
100
- pendingCount++;
101
- if (closed) {
102
- message.reject(
103
- new Error("The bridge turn is no longer accepting user messages.")
104
- );
105
- return;
106
- }
107
- const waiter = waiters.shift();
108
- if (waiter != null) {
109
- waiter({ done: false, value: message });
110
- } else {
111
- messages.push(message);
112
- }
113
- };
114
- const close = (error) => {
115
- if (closed) return;
116
- closed = true;
117
- const reason = error ?? new Error("The bridge turn ended before accepting the user message.");
118
- for (const entry of entries.values()) {
119
- if (entry.response == null) entry.reject(reason);
120
- }
121
- messages.length = 0;
122
- while (waiters.length > 0) {
123
- waiters.shift()({ done: true, value: void 0 });
124
- }
125
- };
126
- return {
127
- get pendingCount() {
128
- return pendingCount;
129
- },
130
- enqueue,
131
- close,
132
- [Symbol.asyncIterator]() {
133
- return {
134
- next: () => {
135
- const message = messages.shift();
136
- if (message != null) {
137
- return Promise.resolve({ done: false, value: message });
138
- }
139
- if (closed) {
140
- return Promise.resolve({
141
- done: true,
142
- value: void 0
143
- });
144
- }
145
- return new Promise(
146
- (resolve) => {
147
- waiters.push(resolve);
148
- }
149
- );
150
- }
151
- };
152
- }
153
- };
51
+ const messages = [];
52
+ const waiters = [];
53
+ const entries = /* @__PURE__ */ new Map();
54
+ let closed = false;
55
+ let pendingCount = 0;
56
+ const enqueue = (input) => {
57
+ const existing = entries.get(input.messageId);
58
+ if (existing != null) {
59
+ if (existing.response != null) options.respond(existing.response);
60
+ return;
61
+ }
62
+ let settled = false;
63
+ const settle = (response) => {
64
+ if (settled) return;
65
+ settled = true;
66
+ pendingCount--;
67
+ const entry = entries.get(input.messageId);
68
+ if (entry != null) entry.response = response;
69
+ options.respond(response);
70
+ };
71
+ const message = {
72
+ messageId: input.messageId,
73
+ text: input.text,
74
+ accept: () => {
75
+ settle({
76
+ type: "user-message-response",
77
+ messageId: input.messageId,
78
+ accepted: true
79
+ });
80
+ },
81
+ reject: (error) => {
82
+ settle({
83
+ type: "user-message-response",
84
+ messageId: input.messageId,
85
+ accepted: false,
86
+ error: { message: formatBridgeError(error).message }
87
+ });
88
+ }
89
+ };
90
+ entries.set(input.messageId, { reject: message.reject });
91
+ pendingCount++;
92
+ if (closed) {
93
+ message.reject(/* @__PURE__ */ new Error("The bridge turn is no longer accepting user messages."));
94
+ return;
95
+ }
96
+ const waiter = waiters.shift();
97
+ if (waiter != null) waiter({
98
+ done: false,
99
+ value: message
100
+ });
101
+ else messages.push(message);
102
+ };
103
+ const close = (error) => {
104
+ if (closed) return;
105
+ closed = true;
106
+ const reason = error ?? /* @__PURE__ */ new Error("The bridge turn ended before accepting the user message.");
107
+ for (const entry of entries.values()) if (entry.response == null) entry.reject(reason);
108
+ messages.length = 0;
109
+ while (waiters.length > 0) waiters.shift()({
110
+ done: true,
111
+ value: void 0
112
+ });
113
+ };
114
+ return {
115
+ get pendingCount() {
116
+ return pendingCount;
117
+ },
118
+ enqueue,
119
+ close,
120
+ [Symbol.asyncIterator]() {
121
+ return { next: () => {
122
+ const message = messages.shift();
123
+ if (message != null) return Promise.resolve({
124
+ done: false,
125
+ value: message
126
+ });
127
+ if (closed) return Promise.resolve({
128
+ done: true,
129
+ value: void 0
130
+ });
131
+ return new Promise((resolve) => {
132
+ waiters.push(resolve);
133
+ });
134
+ } };
135
+ }
136
+ };
154
137
  }
155
138
  function parseEnvList(value) {
156
- if (!value) return void 0;
157
- const items = value.split(",").map((item) => item.trim()).filter(Boolean);
158
- return items.length > 0 ? items : void 0;
139
+ if (!value) return void 0;
140
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean);
141
+ return items.length > 0 ? items : void 0;
159
142
  }
160
- var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
161
- var WS_OPEN = 1;
143
+ const ENV_TRUTHY = /* @__PURE__ */ new Set([
144
+ "1",
145
+ "true",
146
+ "yes",
147
+ "on"
148
+ ]);
149
+ const WS_OPEN = 1;
162
150
  async function runBridge(options) {
163
- const { bridgeType, bridgeStateDir, onStart, onStop, onDestroy } = options;
164
- const teardownGraceMs = options.turnTeardownGraceMs ?? 1e4;
165
- const expectedToken = options.token ?? procEnv.BRIDGE_CHANNEL_TOKEN ?? "";
166
- const bridgeWsPort = options.port ?? parseInt(procEnv.BRIDGE_WS_PORT ?? "0", 10);
167
- const bridgeMetaPath = `${bridgeStateDir}/bridge-meta.json`;
168
- const startConfigPath = `${bridgeStateDir}/start-config.json`;
169
- const rerunStartConfigPath = `${bridgeStateDir}/rerun-start-config.json`;
170
- const eventLogPath = `${bridgeStateDir}/event-log.ndjson`;
171
- try {
172
- await mkdir(bridgeStateDir, { recursive: true });
173
- } catch {
174
- }
175
- let currentBoundPort = 0;
176
- let currentTurnState = "init";
177
- let activeSocket;
178
- let isFirstTurn = true;
179
- let turnAbort;
180
- let currentUserMessages;
181
- let activeTurn;
182
- let debugConfig;
183
- let consoleCaptureInstalled = false;
184
- const envDebugEnabled = ENV_TRUTHY.has(
185
- (procEnv.HARNESS_DEBUG ?? "").toLowerCase()
186
- );
187
- let seqCounter = 0;
188
- let eventLog = [];
189
- let diskBuffer = "";
190
- let flushPromise = null;
191
- const flushEventsToDisk = async () => {
192
- while (diskBuffer.length > 0) {
193
- const buf = diskBuffer;
194
- diskBuffer = "";
195
- await appendFile(eventLogPath, buf).catch(() => {
196
- });
197
- }
198
- };
199
- const scheduleEventFlush = () => {
200
- if (flushPromise) return;
201
- flushPromise = new Promise((resolve) => {
202
- setImmediate(() => {
203
- void flushEventsToDisk().finally(resolve);
204
- });
205
- }).finally(() => {
206
- flushPromise = null;
207
- if (diskBuffer.length > 0) {
208
- scheduleEventFlush();
209
- }
210
- });
211
- };
212
- const flushPendingEventsToDisk = async () => {
213
- if (diskBuffer.length > 0 && !flushPromise) {
214
- scheduleEventFlush();
215
- }
216
- let inFlight = flushPromise;
217
- while (inFlight) {
218
- await inFlight;
219
- inFlight = flushPromise;
220
- }
221
- };
222
- const replayFromDisk = procEnv.BRIDGE_REPLAY_FROM_DISK === "1";
223
- if (replayFromDisk && existsSync(eventLogPath)) {
224
- try {
225
- const lines = readFileSync(eventLogPath, "utf8").split("\n").map((line) => line.trim()).filter(Boolean);
226
- eventLog = lines.map((line) => ({
227
- seq: JSON.parse(line).seq,
228
- line
229
- }));
230
- seqCounter = eventLog.at(-1)?.seq ?? 0;
231
- } catch {
232
- eventLog = [];
233
- seqCounter = 0;
234
- }
235
- }
236
- const pendingToolResults = /* @__PURE__ */ new Map();
237
- const bufferedToolResults = [];
238
- const pendingToolApprovals = /* @__PURE__ */ new Map();
239
- const writeBridgeMeta = async (state) => {
240
- try {
241
- await writeFile(
242
- bridgeMetaPath,
243
- JSON.stringify({
244
- type: bridgeType,
245
- port: currentBoundPort,
246
- state,
247
- pid
248
- })
249
- );
250
- } catch {
251
- }
252
- };
253
- const writeStartConfig = async (start) => {
254
- try {
255
- const serialized = JSON.stringify(start);
256
- await writeFile(startConfigPath, serialized);
257
- if (!existsSync(rerunStartConfigPath)) {
258
- await writeFile(rerunStartConfigPath, serialized);
259
- }
260
- } catch {
261
- }
262
- };
263
- const emit = (event) => {
264
- const seq = ++seqCounter;
265
- const line = JSON.stringify({ ...event, seq });
266
- eventLog.push({ seq, line });
267
- diskBuffer += `${line}
268
- `;
269
- scheduleEventFlush();
270
- if (activeSocket?.readyState === WS_OPEN) {
271
- try {
272
- activeSocket.send(line);
273
- } catch {
274
- }
275
- }
276
- };
277
- const replay = (ws, afterSeq) => {
278
- for (const entry of eventLog) {
279
- if (entry.seq > afterSeq && ws.readyState === WS_OPEN) {
280
- ws.send(entry.line);
281
- }
282
- }
283
- };
284
- const shouldEmitDebugEvent = (level, subsystem) => {
285
- if (!debugConfig?.enabled) return false;
286
- const threshold = debugConfig.level ?? "debug";
287
- if (DEBUG_LEVEL_WEIGHT[level] > DEBUG_LEVEL_WEIGHT[threshold]) return false;
288
- return subsystemMatches(debugConfig.subsystems, subsystem);
289
- };
290
- const rawStdoutWrite = process.stdout.write.bind(process.stdout);
291
- const rawStderrWrite = process.stderr.write.bind(process.stderr);
292
- const writeErrorToStderr = (input) => {
293
- try {
294
- const formatted = formatBridgeError(input.error);
295
- rawStderrWrite(
296
- `[harness:${bridgeType}:error] ${input.message}: ${formatted.message}
297
- `
298
- );
299
- if (formatted.stack) {
300
- rawStderrWrite(`${formatted.stack}
301
- `);
302
- }
303
- } catch {
304
- }
305
- };
306
- const emitWarning = (input) => {
307
- try {
308
- for (const line of input.message.split("\n")) {
309
- if (line.trim().length > 0) {
310
- rawStderrWrite(`[harness:${bridgeType}:warn] ${line}
311
- `);
312
- }
313
- }
314
- } catch {
315
- }
316
- };
317
- const emitError = (input) => {
318
- writeErrorToStderr({
319
- message: input.message ?? "bridge error",
320
- error: input.error
321
- });
322
- emit({ type: "error", error: serialiseError(input.error) });
323
- };
324
- const installConsoleCapture = () => {
325
- if (consoleCaptureInstalled) return;
326
- consoleCaptureInstalled = true;
327
- const buffers = {
328
- stdout: "",
329
- stderr: ""
330
- };
331
- const patch = (stream, raw) => (chunk, encoding, cb) => {
332
- if (debugConfig?.enabled) {
333
- try {
334
- const enc = typeof encoding === "string" ? encoding : "utf8";
335
- const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(
336
- enc
337
- );
338
- const combined = buffers[stream] + text.replace(/\r\n/g, "\n");
339
- const parts = combined.split("\n");
340
- buffers[stream] = parts.pop() ?? "";
341
- for (const line of parts) {
342
- const trimmed = line.replace(/\s+$/, "");
343
- if (trimmed) {
344
- emit({
345
- type: "sandbox-log",
346
- source: bridgeType,
347
- stream,
348
- line: trimmed
349
- });
350
- }
351
- }
352
- } catch {
353
- }
354
- }
355
- return raw(
356
- chunk,
357
- encoding,
358
- cb
359
- );
360
- };
361
- process.stdout.write = patch(
362
- "stdout",
363
- rawStdoutWrite
364
- );
365
- process.stderr.write = patch(
366
- "stderr",
367
- rawStderrWrite
368
- );
369
- };
370
- const handleInbound = async (msg, ws) => {
371
- switch (msg.type) {
372
- case "start": {
373
- for (; ; ) {
374
- const pendingTurn = activeTurn;
375
- if (pendingTurn == null) break;
376
- turnAbort?.abort();
377
- currentUserMessages?.close(
378
- new Error("A new bridge turn replaced the active turn.")
379
- );
380
- let graceTimer;
381
- const settled = await Promise.race([
382
- pendingTurn.then(() => true),
383
- new Promise((resolve) => {
384
- graceTimer = setTimeout(() => resolve(false), teardownGraceMs);
385
- graceTimer.unref?.();
386
- })
387
- ]);
388
- clearTimeout(graceTimer);
389
- if (!settled) break;
390
- }
391
- let turnFinished;
392
- const thisTurn = new Promise((resolve) => turnFinished = resolve);
393
- activeTurn = thisTurn;
394
- activeSocket = ws;
395
- const firstTurn = isFirstTurn;
396
- isFirstTurn = false;
397
- eventLog = [];
398
- diskBuffer = "";
399
- void writeFile(eventLogPath, "").catch(() => {
400
- });
401
- turnAbort = new AbortController();
402
- currentTurnState = "running";
403
- void writeStartConfig(msg);
404
- void writeBridgeMeta("running");
405
- const startDebug = msg.debug;
406
- debugConfig = {
407
- enabled: startDebug?.enabled ?? envDebugEnabled,
408
- level: startDebug?.level ?? procEnv.HARNESS_DEBUG_LEVEL,
409
- subsystems: startDebug?.subsystems ?? parseEnvList(procEnv.HARNESS_DEBUG_SUBSYSTEMS)
410
- };
411
- if (debugConfig.enabled) {
412
- installConsoleCapture();
413
- }
414
- const userMessages = createBridgeUserMessageQueue({ respond: emit });
415
- const turn = {
416
- emit,
417
- requestToolResult: (requestInput) => {
418
- const request = typeof requestInput === "string" ? { toolCallId: requestInput } : requestInput;
419
- const bufferedIndex = bufferedToolResults.findIndex(
420
- (buffered) => buffered.toolCallId === request.toolCallId || request.matches?.(buffered.result) === true
421
- );
422
- if (bufferedIndex >= 0) {
423
- return Promise.resolve(
424
- bufferedToolResults.splice(bufferedIndex, 1)[0].result
425
- );
426
- }
427
- return new Promise((resolve) => {
428
- pendingToolResults.set(request.toolCallId, {
429
- resolve,
430
- matches: request.matches
431
- });
432
- });
433
- },
434
- requestToolApproval: (approvalId) => new Promise((resolve) => {
435
- pendingToolApprovals.set(approvalId, resolve);
436
- }),
437
- experimental_userMessages: userMessages,
438
- abortSignal: turnAbort.signal,
439
- firstTurn,
440
- bridgeLog: (input) => {
441
- const level = input.level ?? "debug";
442
- if (!shouldEmitDebugEvent(level, input.subsystem)) return;
443
- emit({
444
- type: "debug-event",
445
- level,
446
- subsystem: input.subsystem,
447
- message: input.message,
448
- ...input.attrs ? { attrs: input.attrs } : {},
449
- ...input.error !== void 0 ? { error: formatBridgeError(input.error) } : {}
450
- });
451
- },
452
- emitWarning,
453
- emitError
454
- };
455
- currentUserMessages = userMessages;
456
- try {
457
- await onStart(msg, turn);
458
- } catch (err) {
459
- emitError({ error: err, message: "bridge turn failed" });
460
- } finally {
461
- userMessages.close();
462
- if (currentUserMessages === userMessages) {
463
- currentUserMessages = void 0;
464
- }
465
- if (activeTurn === thisTurn) {
466
- activeTurn = void 0;
467
- currentTurnState = "waiting";
468
- void writeBridgeMeta("waiting");
469
- }
470
- turnFinished();
471
- }
472
- return;
473
- }
474
- case "tool-result": {
475
- const result = {
476
- output: msg.output,
477
- isError: msg.isError,
478
- toolResult: msg.toolResult
479
- };
480
- const exactPending = pendingToolResults.get(msg.toolCallId);
481
- const matchingPending = exactPending == null ? Array.from(pendingToolResults.entries()).find(
482
- ([, pending2]) => pending2.matches?.(result) === true
483
- ) : void 0;
484
- const pending = exactPending ?? matchingPending?.[1];
485
- const pendingId = exactPending != null ? msg.toolCallId : matchingPending?.[0];
486
- if (pending != null && pendingId != null) {
487
- pendingToolResults.delete(pendingId);
488
- pending.resolve(result);
489
- } else {
490
- bufferedToolResults.push({
491
- toolCallId: msg.toolCallId,
492
- result
493
- });
494
- }
495
- return;
496
- }
497
- case "tool-approval-response": {
498
- const resolver = pendingToolApprovals.get(msg.approvalId);
499
- if (resolver) {
500
- pendingToolApprovals.delete(msg.approvalId);
501
- resolver({ approved: msg.approved, reason: msg.reason });
502
- }
503
- return;
504
- }
505
- case "user-message": {
506
- const messageId = msg.messageId ?? randomUUID();
507
- if (currentUserMessages == null) {
508
- sendControl(ws, {
509
- type: "user-message-response",
510
- messageId,
511
- accepted: false,
512
- error: { message: "The bridge has no active turn to steer." }
513
- });
514
- return;
515
- }
516
- if (ws !== activeSocket) {
517
- sendControl(ws, {
518
- type: "user-message-response",
519
- messageId,
520
- accepted: false,
521
- error: {
522
- message: "The connection does not own the active bridge turn."
523
- }
524
- });
525
- return;
526
- }
527
- currentUserMessages.enqueue({
528
- messageId,
529
- text: msg.text
530
- });
531
- return;
532
- }
533
- case "abort":
534
- turnAbort?.abort();
535
- return;
536
- case "resume":
537
- activeSocket = ws;
538
- replay(ws, msg.lastSeenEventId);
539
- return;
540
- case "destroy":
541
- currentTurnState = "done";
542
- void writeBridgeMeta("done");
543
- await onDestroy?.();
544
- drainThenExit(ws, 1e3, "destroy");
545
- return;
546
- case "stop": {
547
- currentTurnState = "done";
548
- void writeBridgeMeta("done");
549
- const data = await onStop?.() ?? {};
550
- sendControl(ws, { type: "bridge-stop", data });
551
- drainThenExit(ws, 1e3, "stop");
552
- }
553
- }
554
- };
555
- void writeBridgeMeta("init");
556
- const wss = new WebSocketServer({ port: bridgeWsPort, host: "0.0.0.0" });
557
- const exit = () => {
558
- if (options.onExit) {
559
- options.onExit();
560
- return;
561
- }
562
- wss.close(() => process.exit(0));
563
- setTimeout(() => process.exit(0), 1e3).unref();
564
- };
565
- const drainThenExit = (ws, code, reason) => {
566
- const start = Date.now();
567
- const tick = () => {
568
- const drained = ws.bufferedAmount === 0 || ws.readyState !== WS_OPEN;
569
- if (drained || Date.now() - start >= 5e3) {
570
- void flushPendingEventsToDisk().finally(() => {
571
- try {
572
- ws.close(code, reason);
573
- } finally {
574
- exit();
575
- }
576
- });
577
- return;
578
- }
579
- setTimeout(tick, 10).unref();
580
- };
581
- tick();
582
- };
583
- wss.on("listening", () => {
584
- const addr = wss.address();
585
- currentBoundPort = typeof addr === "object" && addr ? addr.port : 0;
586
- currentTurnState = "waiting";
587
- void writeBridgeMeta("waiting");
588
- stdout.write(
589
- JSON.stringify({
590
- type: "bridge-ready",
591
- port: currentBoundPort
592
- }) + "\n"
593
- );
594
- options.onListening?.(currentBoundPort);
595
- });
596
- wss.on("connection", (ws, req) => {
597
- const url = new URL(req.url ?? "/", "http://localhost");
598
- if (url.searchParams.get("agent_bridge_token") !== expectedToken) {
599
- ws.close(1008, "unauthorized");
600
- return;
601
- }
602
- sendControl(ws, {
603
- type: "bridge-hello",
604
- state: currentTurnState,
605
- lastSeq: seqCounter,
606
- capabilities: { experimental_userMessageResponses: true }
607
- });
608
- ws.on("message", (raw) => {
609
- let parsed;
610
- try {
611
- const text = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
612
- parsed = JSON.parse(text);
613
- } catch (err) {
614
- sendControl(ws, {
615
- type: "error",
616
- error: `protocol parse error: ${err.message}`
617
- });
618
- return;
619
- }
620
- void handleInbound(parsed, ws);
621
- });
622
- ws.on("close", () => {
623
- if (activeSocket === ws) {
624
- activeSocket = void 0;
625
- }
626
- });
627
- ws.on("error", () => {
628
- });
629
- });
630
- process.on("uncaughtException", (err) => {
631
- emitError({ error: err, message: "uncaught exception" });
632
- });
633
- process.on("unhandledRejection", (err) => {
634
- emitError({ error: err, message: "unhandled rejection" });
635
- });
636
- await new Promise((resolve, reject) => {
637
- if (wss.address() != null) {
638
- resolve();
639
- return;
640
- }
641
- wss.once("listening", resolve);
642
- wss.once("error", reject);
643
- });
644
- return {
645
- port: currentBoundPort,
646
- close: () => new Promise((resolve) => {
647
- wss.close(() => resolve());
648
- })
649
- };
151
+ const { bridgeType, bridgeStateDir, onStart, onStop, onDestroy } = options;
152
+ const teardownGraceMs = options.turnTeardownGraceMs ?? 1e4;
153
+ const expectedToken = options.token ?? env.BRIDGE_CHANNEL_TOKEN ?? "";
154
+ const bridgeWsPort = options.port ?? parseInt(env.BRIDGE_WS_PORT ?? "0", 10);
155
+ const bridgeMetaPath = `${bridgeStateDir}/bridge-meta.json`;
156
+ const startConfigPath = `${bridgeStateDir}/start-config.json`;
157
+ const rerunStartConfigPath = `${bridgeStateDir}/rerun-start-config.json`;
158
+ const eventLogPath = `${bridgeStateDir}/event-log.ndjson`;
159
+ try {
160
+ await mkdir(bridgeStateDir, { recursive: true });
161
+ } catch {}
162
+ let currentBoundPort = 0;
163
+ let currentTurnState = "init";
164
+ let activeSocket;
165
+ let isFirstTurn = true;
166
+ let turnAbort;
167
+ let currentUserMessages;
168
+ /**
169
+ * Settles when the in-flight turn has fully wound down — `onStart`
170
+ * returned or threw AND its completion state was recorded. `undefined`
171
+ * between turns. A new `start` fences on this so turns never overlap.
172
+ */
173
+ let activeTurn;
174
+ let debugConfig;
175
+ let consoleCaptureInstalled = false;
176
+ const envDebugEnabled = ENV_TRUTHY.has((env.HARNESS_DEBUG ?? "").toLowerCase());
177
+ let seqCounter = 0;
178
+ let eventLog = [];
179
+ let diskBuffer = "";
180
+ let flushPromise = null;
181
+ const flushEventsToDisk = async () => {
182
+ while (diskBuffer.length > 0) {
183
+ const buf = diskBuffer;
184
+ diskBuffer = "";
185
+ await appendFile(eventLogPath, buf).catch(() => {});
186
+ }
187
+ };
188
+ const scheduleEventFlush = () => {
189
+ if (flushPromise) return;
190
+ flushPromise = new Promise((resolve) => {
191
+ setImmediate(() => {
192
+ flushEventsToDisk().finally(resolve);
193
+ });
194
+ }).finally(() => {
195
+ flushPromise = null;
196
+ if (diskBuffer.length > 0) scheduleEventFlush();
197
+ });
198
+ };
199
+ const flushPendingEventsToDisk = async () => {
200
+ if (diskBuffer.length > 0 && !flushPromise) scheduleEventFlush();
201
+ let inFlight = flushPromise;
202
+ while (inFlight) {
203
+ await inFlight;
204
+ inFlight = flushPromise;
205
+ }
206
+ };
207
+ if (env.BRIDGE_REPLAY_FROM_DISK === "1" && existsSync(eventLogPath)) try {
208
+ eventLog = readFileSync(eventLogPath, "utf8").split("\n").map((line) => line.trim()).filter(Boolean).map((line) => ({
209
+ seq: JSON.parse(line).seq,
210
+ line
211
+ }));
212
+ seqCounter = eventLog.at(-1)?.seq ?? 0;
213
+ } catch {
214
+ eventLog = [];
215
+ seqCounter = 0;
216
+ }
217
+ const pendingToolResults = /* @__PURE__ */ new Map();
218
+ const bufferedToolResults = [];
219
+ const pendingToolApprovals = /* @__PURE__ */ new Map();
220
+ const writeBridgeMeta = async (state) => {
221
+ try {
222
+ await writeFile(bridgeMetaPath, JSON.stringify({
223
+ type: bridgeType,
224
+ port: currentBoundPort,
225
+ state,
226
+ pid
227
+ }));
228
+ } catch {}
229
+ };
230
+ const writeStartConfig = async (start) => {
231
+ try {
232
+ const serialized = JSON.stringify(start);
233
+ await writeFile(startConfigPath, serialized);
234
+ if (!existsSync(rerunStartConfigPath)) await writeFile(rerunStartConfigPath, serialized);
235
+ } catch {}
236
+ };
237
+ const emit = (event) => {
238
+ const seq = ++seqCounter;
239
+ const line = JSON.stringify({
240
+ ...event,
241
+ seq
242
+ });
243
+ eventLog.push({
244
+ seq,
245
+ line
246
+ });
247
+ diskBuffer += `${line}\n`;
248
+ scheduleEventFlush();
249
+ if (activeSocket?.readyState === WS_OPEN) try {
250
+ activeSocket.send(line);
251
+ } catch {}
252
+ };
253
+ const replay = (ws, afterSeq) => {
254
+ for (const entry of eventLog) if (entry.seq > afterSeq && ws.readyState === WS_OPEN) ws.send(entry.line);
255
+ };
256
+ const shouldEmitDebugEvent = (level, subsystem) => {
257
+ if (!debugConfig?.enabled) return false;
258
+ const threshold = debugConfig.level ?? "debug";
259
+ if (DEBUG_LEVEL_WEIGHT[level] > DEBUG_LEVEL_WEIGHT[threshold]) return false;
260
+ return subsystemMatches(debugConfig.subsystems, subsystem);
261
+ };
262
+ const rawStdoutWrite = process.stdout.write.bind(process.stdout);
263
+ const rawStderrWrite = process.stderr.write.bind(process.stderr);
264
+ const writeErrorToStderr = (input) => {
265
+ try {
266
+ const formatted = formatBridgeError(input.error);
267
+ rawStderrWrite(`[harness:${bridgeType}:error] ${input.message}: ${formatted.message}\n`);
268
+ if (formatted.stack) rawStderrWrite(`${formatted.stack}\n`);
269
+ } catch {}
270
+ };
271
+ const emitWarning = (input) => {
272
+ try {
273
+ for (const line of input.message.split("\n")) if (line.trim().length > 0) rawStderrWrite(`[harness:${bridgeType}:warn] ${line}\n`);
274
+ } catch {}
275
+ };
276
+ const emitError = (input) => {
277
+ writeErrorToStderr({
278
+ message: input.message ?? "bridge error",
279
+ error: input.error
280
+ });
281
+ emit({
282
+ type: "error",
283
+ error: serialiseError(input.error)
284
+ });
285
+ };
286
+ const installConsoleCapture = () => {
287
+ if (consoleCaptureInstalled) return;
288
+ consoleCaptureInstalled = true;
289
+ const buffers = {
290
+ stdout: "",
291
+ stderr: ""
292
+ };
293
+ const patch = (stream, raw) => (chunk, encoding, cb) => {
294
+ if (debugConfig?.enabled) try {
295
+ const enc = typeof encoding === "string" ? encoding : "utf8";
296
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(enc);
297
+ const parts = (buffers[stream] + text.replace(/\r\n/g, "\n")).split("\n");
298
+ buffers[stream] = parts.pop() ?? "";
299
+ for (const line of parts) {
300
+ const trimmed = line.replace(/\s+$/, "");
301
+ if (trimmed) emit({
302
+ type: "sandbox-log",
303
+ source: bridgeType,
304
+ stream,
305
+ line: trimmed
306
+ });
307
+ }
308
+ } catch {}
309
+ return raw(chunk, encoding, cb);
310
+ };
311
+ process.stdout.write = patch("stdout", rawStdoutWrite);
312
+ process.stderr.write = patch("stderr", rawStderrWrite);
313
+ };
314
+ const handleInbound = async (msg, ws) => {
315
+ switch (msg.type) {
316
+ case "start": {
317
+ for (;;) {
318
+ const pendingTurn = activeTurn;
319
+ if (pendingTurn == null) break;
320
+ turnAbort?.abort();
321
+ currentUserMessages?.close(/* @__PURE__ */ new Error("A new bridge turn replaced the active turn."));
322
+ let graceTimer;
323
+ const settled = await Promise.race([pendingTurn.then(() => true), new Promise((resolve) => {
324
+ graceTimer = setTimeout(() => resolve(false), teardownGraceMs);
325
+ graceTimer.unref?.();
326
+ })]);
327
+ clearTimeout(graceTimer);
328
+ if (!settled) break;
329
+ }
330
+ let turnFinished;
331
+ const thisTurn = new Promise((resolve) => turnFinished = resolve);
332
+ activeTurn = thisTurn;
333
+ activeSocket = ws;
334
+ const firstTurn = isFirstTurn;
335
+ isFirstTurn = false;
336
+ eventLog = [];
337
+ diskBuffer = "";
338
+ writeFile(eventLogPath, "").catch(() => {});
339
+ turnAbort = new AbortController();
340
+ currentTurnState = "running";
341
+ writeStartConfig(msg);
342
+ writeBridgeMeta("running");
343
+ const startDebug = msg.debug;
344
+ debugConfig = {
345
+ enabled: startDebug?.enabled ?? envDebugEnabled,
346
+ level: startDebug?.level ?? env.HARNESS_DEBUG_LEVEL,
347
+ subsystems: startDebug?.subsystems ?? parseEnvList(env.HARNESS_DEBUG_SUBSYSTEMS)
348
+ };
349
+ if (debugConfig.enabled) installConsoleCapture();
350
+ const userMessages = createBridgeUserMessageQueue({ respond: emit });
351
+ const turn = {
352
+ emit,
353
+ requestToolResult: (requestInput) => {
354
+ const request = typeof requestInput === "string" ? { toolCallId: requestInput } : requestInput;
355
+ const bufferedIndex = bufferedToolResults.findIndex((buffered) => buffered.toolCallId === request.toolCallId || request.matches?.(buffered.result) === true);
356
+ if (bufferedIndex >= 0) return Promise.resolve(bufferedToolResults.splice(bufferedIndex, 1)[0].result);
357
+ return new Promise((resolve) => {
358
+ pendingToolResults.set(request.toolCallId, {
359
+ resolve,
360
+ matches: request.matches
361
+ });
362
+ });
363
+ },
364
+ requestToolApproval: (approvalId) => new Promise((resolve) => {
365
+ pendingToolApprovals.set(approvalId, resolve);
366
+ }),
367
+ experimental_userMessages: userMessages,
368
+ abortSignal: turnAbort.signal,
369
+ firstTurn,
370
+ bridgeLog: (input) => {
371
+ const level = input.level ?? "debug";
372
+ if (!shouldEmitDebugEvent(level, input.subsystem)) return;
373
+ emit({
374
+ type: "debug-event",
375
+ level,
376
+ subsystem: input.subsystem,
377
+ message: input.message,
378
+ ...input.attrs ? { attrs: input.attrs } : {},
379
+ ...input.error !== void 0 ? { error: formatBridgeError(input.error) } : {}
380
+ });
381
+ },
382
+ emitWarning,
383
+ emitError
384
+ };
385
+ currentUserMessages = userMessages;
386
+ try {
387
+ await onStart(msg, turn);
388
+ } catch (err) {
389
+ emitError({
390
+ error: err,
391
+ message: "bridge turn failed"
392
+ });
393
+ } finally {
394
+ userMessages.close();
395
+ if (currentUserMessages === userMessages) currentUserMessages = void 0;
396
+ if (activeTurn === thisTurn) {
397
+ activeTurn = void 0;
398
+ currentTurnState = "waiting";
399
+ writeBridgeMeta("waiting");
400
+ }
401
+ turnFinished();
402
+ }
403
+ return;
404
+ }
405
+ case "tool-result": {
406
+ const result = {
407
+ output: msg.output,
408
+ isError: msg.isError,
409
+ toolResult: msg.toolResult
410
+ };
411
+ const exactPending = pendingToolResults.get(msg.toolCallId);
412
+ const matchingPending = exactPending == null ? Array.from(pendingToolResults.entries()).find(([, pending]) => pending.matches?.(result) === true) : void 0;
413
+ const pending = exactPending ?? matchingPending?.[1];
414
+ const pendingId = exactPending != null ? msg.toolCallId : matchingPending?.[0];
415
+ if (pending != null && pendingId != null) {
416
+ pendingToolResults.delete(pendingId);
417
+ pending.resolve(result);
418
+ } else bufferedToolResults.push({
419
+ toolCallId: msg.toolCallId,
420
+ result
421
+ });
422
+ return;
423
+ }
424
+ case "tool-approval-response": {
425
+ const resolver = pendingToolApprovals.get(msg.approvalId);
426
+ if (resolver) {
427
+ pendingToolApprovals.delete(msg.approvalId);
428
+ resolver({
429
+ approved: msg.approved,
430
+ reason: msg.reason
431
+ });
432
+ }
433
+ return;
434
+ }
435
+ case "user-message": {
436
+ const messageId = msg.messageId ?? randomUUID();
437
+ if (currentUserMessages == null) {
438
+ sendControl(ws, {
439
+ type: "user-message-response",
440
+ messageId,
441
+ accepted: false,
442
+ error: { message: "The bridge has no active turn to steer." }
443
+ });
444
+ return;
445
+ }
446
+ if (ws !== activeSocket) {
447
+ sendControl(ws, {
448
+ type: "user-message-response",
449
+ messageId,
450
+ accepted: false,
451
+ error: { message: "The connection does not own the active bridge turn." }
452
+ });
453
+ return;
454
+ }
455
+ currentUserMessages.enqueue({
456
+ messageId,
457
+ text: msg.text
458
+ });
459
+ return;
460
+ }
461
+ case "abort":
462
+ turnAbort?.abort();
463
+ return;
464
+ case "resume":
465
+ activeSocket = ws;
466
+ replay(ws, msg.lastSeenEventId);
467
+ return;
468
+ case "destroy":
469
+ currentTurnState = "done";
470
+ writeBridgeMeta("done");
471
+ await onDestroy?.();
472
+ drainThenExit(ws, 1e3, "destroy");
473
+ return;
474
+ case "stop":
475
+ currentTurnState = "done";
476
+ writeBridgeMeta("done");
477
+ sendControl(ws, {
478
+ type: "bridge-stop",
479
+ data: await onStop?.() ?? {}
480
+ });
481
+ drainThenExit(ws, 1e3, "stop");
482
+ }
483
+ };
484
+ writeBridgeMeta("init");
485
+ const wss = new WebSocketServer({
486
+ port: bridgeWsPort,
487
+ host: "0.0.0.0"
488
+ });
489
+ const exit = () => {
490
+ if (options.onExit) {
491
+ options.onExit();
492
+ return;
493
+ }
494
+ wss.close(() => process.exit(0));
495
+ setTimeout(() => process.exit(0), 1e3).unref();
496
+ };
497
+ const drainThenExit = (ws, code, reason) => {
498
+ const start = Date.now();
499
+ const tick = () => {
500
+ if (ws.bufferedAmount === 0 || ws.readyState !== WS_OPEN || Date.now() - start >= 5e3) {
501
+ flushPendingEventsToDisk().finally(() => {
502
+ try {
503
+ ws.close(code, reason);
504
+ } finally {
505
+ exit();
506
+ }
507
+ });
508
+ return;
509
+ }
510
+ setTimeout(tick, 10).unref();
511
+ };
512
+ tick();
513
+ };
514
+ wss.on("listening", () => {
515
+ const addr = wss.address();
516
+ currentBoundPort = typeof addr === "object" && addr ? addr.port : 0;
517
+ currentTurnState = "waiting";
518
+ writeBridgeMeta("waiting");
519
+ stdout.write(JSON.stringify({
520
+ type: "bridge-ready",
521
+ port: currentBoundPort
522
+ }) + "\n");
523
+ options.onListening?.(currentBoundPort);
524
+ });
525
+ wss.on("connection", (ws, req) => {
526
+ if (new URL(req.url ?? "/", "http://localhost").searchParams.get("agent_bridge_token") !== expectedToken) {
527
+ ws.close(1008, "unauthorized");
528
+ return;
529
+ }
530
+ sendControl(ws, {
531
+ type: "bridge-hello",
532
+ state: currentTurnState,
533
+ lastSeq: seqCounter,
534
+ capabilities: { experimental_userMessageResponses: true }
535
+ });
536
+ ws.on("message", (raw) => {
537
+ let parsed;
538
+ try {
539
+ const text = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
540
+ parsed = JSON.parse(text);
541
+ } catch (err) {
542
+ sendControl(ws, {
543
+ type: "error",
544
+ error: `protocol parse error: ${err.message}`
545
+ });
546
+ return;
547
+ }
548
+ handleInbound(parsed, ws);
549
+ });
550
+ ws.on("close", () => {
551
+ if (activeSocket === ws) activeSocket = void 0;
552
+ });
553
+ ws.on("error", () => {});
554
+ });
555
+ process.on("uncaughtException", (err) => {
556
+ emitError({
557
+ error: err,
558
+ message: "uncaught exception"
559
+ });
560
+ });
561
+ process.on("unhandledRejection", (err) => {
562
+ emitError({
563
+ error: err,
564
+ message: "unhandled rejection"
565
+ });
566
+ });
567
+ await new Promise((resolve, reject) => {
568
+ if (wss.address() != null) {
569
+ resolve();
570
+ return;
571
+ }
572
+ wss.once("listening", resolve);
573
+ wss.once("error", reject);
574
+ });
575
+ return {
576
+ port: currentBoundPort,
577
+ close: () => new Promise((resolve) => {
578
+ wss.close(() => resolve());
579
+ })
580
+ };
650
581
  }
651
582
  function sendControl(socket, message) {
652
- if (socket?.readyState === WS_OPEN) {
653
- try {
654
- socket.send(JSON.stringify(message));
655
- } catch {
656
- }
657
- }
583
+ if (socket?.readyState === WS_OPEN) try {
584
+ socket.send(JSON.stringify(message));
585
+ } catch {}
658
586
  }
659
587
  function serialiseError(err) {
660
- if (err instanceof Error) {
661
- return { name: err.name, message: err.message, stack: err.stack };
662
- }
663
- return err;
588
+ if (err instanceof Error) return {
589
+ name: err.name,
590
+ message: err.message,
591
+ stack: err.stack
592
+ };
593
+ return err;
664
594
  }
665
- export {
666
- HarnessBridgeCapabilityUnsupportedError,
667
- runBridge
668
- };
595
+ //#endregion
596
+ export { HarnessBridgeCapabilityUnsupportedError, runBridge };
597
+
669
598
  //# sourceMappingURL=index.js.map