@ai-sdk/harness-codex 0.0.0 → 1.0.0-beta.11

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.
@@ -0,0 +1,924 @@
1
+ // ../harness/dist/bridge/index.js
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
+ return { message: String(err) };
24
+ }
25
+ function parseEnvList(value) {
26
+ if (!value) return void 0;
27
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean);
28
+ return items.length > 0 ? items : void 0;
29
+ }
30
+ var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
31
+ var WS_OPEN = 1;
32
+ async function runBridge(options) {
33
+ const { bridgeType, bridgeStateDir: bridgeStateDir2, onStart, onDetach } = options;
34
+ const expectedToken = options.token ?? procEnv.BRIDGE_CHANNEL_TOKEN ?? "";
35
+ const bridgeWsPort = options.port ?? parseInt(procEnv.BRIDGE_WS_PORT ?? "0", 10);
36
+ const bridgeMetaPath = `${bridgeStateDir2}/bridge-meta.json`;
37
+ const startConfigPath = `${bridgeStateDir2}/start-config.json`;
38
+ const rerunStartConfigPath = `${bridgeStateDir2}/rerun-start-config.json`;
39
+ const eventLogPath = `${bridgeStateDir2}/event-log.ndjson`;
40
+ try {
41
+ await mkdir(bridgeStateDir2, { recursive: true });
42
+ } catch {
43
+ }
44
+ let currentBoundPort = 0;
45
+ let currentTurnState = "init";
46
+ let activeSocket;
47
+ let isFirstTurn = true;
48
+ let turnAbort;
49
+ let currentUserMessages;
50
+ let debugConfig;
51
+ let consoleCaptureInstalled = false;
52
+ const envDebugEnabled = ENV_TRUTHY.has(
53
+ (procEnv.HARNESS_DEBUG ?? "").toLowerCase()
54
+ );
55
+ let seqCounter = 0;
56
+ let eventLog = [];
57
+ let diskBuffer = "";
58
+ let flushPromise = null;
59
+ const flushEventsToDisk = async () => {
60
+ while (diskBuffer.length > 0) {
61
+ const buf = diskBuffer;
62
+ diskBuffer = "";
63
+ await appendFile(eventLogPath, buf).catch(() => {
64
+ });
65
+ }
66
+ };
67
+ const scheduleEventFlush = () => {
68
+ if (flushPromise) return;
69
+ flushPromise = new Promise((resolve) => {
70
+ setImmediate(() => {
71
+ void flushEventsToDisk().finally(resolve);
72
+ });
73
+ }).finally(() => {
74
+ flushPromise = null;
75
+ if (diskBuffer.length > 0) {
76
+ scheduleEventFlush();
77
+ }
78
+ });
79
+ };
80
+ const flushPendingEventsToDisk = async () => {
81
+ if (diskBuffer.length > 0 && !flushPromise) {
82
+ scheduleEventFlush();
83
+ }
84
+ let inFlight = flushPromise;
85
+ while (inFlight) {
86
+ await inFlight;
87
+ inFlight = flushPromise;
88
+ }
89
+ };
90
+ const replayFromDisk = procEnv.BRIDGE_REPLAY_FROM_DISK === "1";
91
+ if (replayFromDisk && existsSync(eventLogPath)) {
92
+ try {
93
+ const lines = readFileSync(eventLogPath, "utf8").split("\n").map((line) => line.trim()).filter(Boolean);
94
+ eventLog = lines.map((line) => ({
95
+ seq: JSON.parse(line).seq,
96
+ line
97
+ }));
98
+ seqCounter = eventLog.at(-1)?.seq ?? 0;
99
+ } catch {
100
+ eventLog = [];
101
+ seqCounter = 0;
102
+ }
103
+ }
104
+ const pendingToolResults = /* @__PURE__ */ new Map();
105
+ const pendingToolApprovals = /* @__PURE__ */ new Map();
106
+ const writeBridgeMeta = async (state) => {
107
+ try {
108
+ await writeFile(
109
+ bridgeMetaPath,
110
+ JSON.stringify({
111
+ type: bridgeType,
112
+ port: currentBoundPort,
113
+ state,
114
+ pid
115
+ })
116
+ );
117
+ } catch {
118
+ }
119
+ };
120
+ const writeStartConfig = async (start) => {
121
+ try {
122
+ const serialized = JSON.stringify(start);
123
+ await writeFile(startConfigPath, serialized);
124
+ if (!existsSync(rerunStartConfigPath)) {
125
+ await writeFile(rerunStartConfigPath, serialized);
126
+ }
127
+ } catch {
128
+ }
129
+ };
130
+ const sendControl = (msg) => {
131
+ if (activeSocket?.readyState === WS_OPEN) {
132
+ try {
133
+ activeSocket.send(JSON.stringify(msg));
134
+ } catch {
135
+ }
136
+ }
137
+ };
138
+ const emit = (event) => {
139
+ const seq = ++seqCounter;
140
+ const line = JSON.stringify({ ...event, seq });
141
+ eventLog.push({ seq, line });
142
+ diskBuffer += `${line}
143
+ `;
144
+ scheduleEventFlush();
145
+ if (activeSocket?.readyState === WS_OPEN) {
146
+ try {
147
+ activeSocket.send(line);
148
+ } catch {
149
+ }
150
+ }
151
+ };
152
+ const replay = (ws, afterSeq) => {
153
+ for (const entry of eventLog) {
154
+ if (entry.seq > afterSeq && ws.readyState === WS_OPEN) {
155
+ ws.send(entry.line);
156
+ }
157
+ }
158
+ };
159
+ const shouldEmitDebugEvent = (level, subsystem) => {
160
+ if (!debugConfig?.enabled) return false;
161
+ const threshold = debugConfig.level ?? "debug";
162
+ if (DEBUG_LEVEL_WEIGHT[level] > DEBUG_LEVEL_WEIGHT[threshold]) return false;
163
+ return subsystemMatches(debugConfig.subsystems, subsystem);
164
+ };
165
+ const rawStdoutWrite = process.stdout.write.bind(process.stdout);
166
+ const rawStderrWrite = process.stderr.write.bind(process.stderr);
167
+ const installConsoleCapture = () => {
168
+ if (consoleCaptureInstalled) return;
169
+ consoleCaptureInstalled = true;
170
+ const buffers = {
171
+ stdout: "",
172
+ stderr: ""
173
+ };
174
+ const patch = (stream, raw) => (chunk, encoding, cb) => {
175
+ if (debugConfig?.enabled) {
176
+ try {
177
+ const enc = typeof encoding === "string" ? encoding : "utf8";
178
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(
179
+ enc
180
+ );
181
+ const combined = buffers[stream] + text.replace(/\r\n/g, "\n");
182
+ const parts = combined.split("\n");
183
+ buffers[stream] = parts.pop() ?? "";
184
+ for (const line of parts) {
185
+ const trimmed = line.replace(/\s+$/, "");
186
+ if (trimmed) {
187
+ emit({
188
+ type: "sandbox-log",
189
+ source: bridgeType,
190
+ stream,
191
+ line: trimmed
192
+ });
193
+ }
194
+ }
195
+ } catch {
196
+ }
197
+ }
198
+ return raw(
199
+ chunk,
200
+ encoding,
201
+ cb
202
+ );
203
+ };
204
+ process.stdout.write = patch(
205
+ "stdout",
206
+ rawStdoutWrite
207
+ );
208
+ process.stderr.write = patch(
209
+ "stderr",
210
+ rawStderrWrite
211
+ );
212
+ };
213
+ const handleInbound = async (msg, ws) => {
214
+ switch (msg.type) {
215
+ case "start": {
216
+ const firstTurn = isFirstTurn;
217
+ isFirstTurn = false;
218
+ eventLog = [];
219
+ diskBuffer = "";
220
+ void writeFile(eventLogPath, "").catch(() => {
221
+ });
222
+ turnAbort = new AbortController();
223
+ currentTurnState = "running";
224
+ void writeStartConfig(msg);
225
+ void writeBridgeMeta("running");
226
+ const startDebug = msg.debug;
227
+ debugConfig = {
228
+ enabled: startDebug?.enabled ?? envDebugEnabled,
229
+ level: startDebug?.level ?? procEnv.HARNESS_DEBUG_LEVEL,
230
+ subsystems: startDebug?.subsystems ?? parseEnvList(procEnv.HARNESS_DEBUG_SUBSYSTEMS)
231
+ };
232
+ if (debugConfig.enabled) {
233
+ installConsoleCapture();
234
+ }
235
+ const turn = {
236
+ emit,
237
+ requestToolResult: (toolCallId) => new Promise((resolve) => {
238
+ pendingToolResults.set(toolCallId, resolve);
239
+ }),
240
+ requestToolApproval: (approvalId) => new Promise((resolve) => {
241
+ pendingToolApprovals.set(approvalId, resolve);
242
+ }),
243
+ pendingUserMessages: [],
244
+ abortSignal: turnAbort.signal,
245
+ firstTurn,
246
+ bridgeLog: (input) => {
247
+ const level = input.level ?? "debug";
248
+ if (!shouldEmitDebugEvent(level, input.subsystem)) return;
249
+ emit({
250
+ type: "debug-event",
251
+ level,
252
+ subsystem: input.subsystem,
253
+ message: input.message,
254
+ ...input.attrs ? { attrs: input.attrs } : {},
255
+ ...input.error !== void 0 ? { error: formatBridgeError(input.error) } : {}
256
+ });
257
+ }
258
+ };
259
+ currentUserMessages = turn.pendingUserMessages;
260
+ try {
261
+ await onStart(msg, turn);
262
+ } catch (err) {
263
+ emit({ type: "error", error: serialiseError(err) });
264
+ } finally {
265
+ currentTurnState = "waiting";
266
+ void writeBridgeMeta("waiting");
267
+ }
268
+ return;
269
+ }
270
+ case "tool-result": {
271
+ const resolver = pendingToolResults.get(msg.toolCallId);
272
+ if (resolver) {
273
+ pendingToolResults.delete(msg.toolCallId);
274
+ resolver({ output: msg.output, isError: msg.isError });
275
+ }
276
+ return;
277
+ }
278
+ case "tool-approval-response": {
279
+ const resolver = pendingToolApprovals.get(msg.approvalId);
280
+ if (resolver) {
281
+ pendingToolApprovals.delete(msg.approvalId);
282
+ resolver({ approved: msg.approved, reason: msg.reason });
283
+ }
284
+ return;
285
+ }
286
+ case "user-message":
287
+ currentUserMessages?.push(msg.text);
288
+ return;
289
+ case "abort":
290
+ turnAbort?.abort();
291
+ return;
292
+ case "resume":
293
+ replay(ws, msg.lastSeenEventId);
294
+ return;
295
+ case "shutdown":
296
+ currentTurnState = "done";
297
+ void writeBridgeMeta("done");
298
+ drainThenExit(ws, 1e3, "shutdown");
299
+ return;
300
+ case "detach": {
301
+ currentTurnState = "done";
302
+ void writeBridgeMeta("done");
303
+ const data = await onDetach?.() ?? {};
304
+ sendControl({ type: "bridge-detach", data });
305
+ drainThenExit(ws, 1e3, "detach");
306
+ return;
307
+ }
308
+ }
309
+ };
310
+ void writeBridgeMeta("init");
311
+ const wss = new WebSocketServer({ port: bridgeWsPort, host: "0.0.0.0" });
312
+ const exit = () => {
313
+ if (options.onExit) {
314
+ options.onExit();
315
+ return;
316
+ }
317
+ wss.close(() => process.exit(0));
318
+ setTimeout(() => process.exit(0), 1e3).unref();
319
+ };
320
+ const drainThenExit = (ws, code, reason) => {
321
+ const start = Date.now();
322
+ const tick = () => {
323
+ const drained = ws.bufferedAmount === 0 || ws.readyState !== WS_OPEN;
324
+ if (drained || Date.now() - start >= 5e3) {
325
+ void flushPendingEventsToDisk().finally(() => {
326
+ try {
327
+ ws.close(code, reason);
328
+ } finally {
329
+ exit();
330
+ }
331
+ });
332
+ return;
333
+ }
334
+ setTimeout(tick, 10).unref();
335
+ };
336
+ tick();
337
+ };
338
+ wss.on("listening", () => {
339
+ const addr = wss.address();
340
+ currentBoundPort = typeof addr === "object" && addr ? addr.port : 0;
341
+ currentTurnState = "waiting";
342
+ void writeBridgeMeta("waiting");
343
+ stdout.write(
344
+ JSON.stringify({
345
+ type: "bridge-ready",
346
+ port: currentBoundPort
347
+ }) + "\n"
348
+ );
349
+ options.onListening?.(currentBoundPort);
350
+ });
351
+ wss.on("connection", (ws, req) => {
352
+ const url = new URL(req.url ?? "/", "http://localhost");
353
+ if (url.searchParams.get("agent_bridge_token") !== expectedToken) {
354
+ ws.close(1008, "unauthorized");
355
+ return;
356
+ }
357
+ activeSocket = ws;
358
+ sendControl({
359
+ type: "bridge-hello",
360
+ state: currentTurnState,
361
+ lastSeq: seqCounter
362
+ });
363
+ ws.on("message", (raw) => {
364
+ let parsed;
365
+ try {
366
+ const text = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
367
+ parsed = JSON.parse(text);
368
+ } catch (err) {
369
+ sendControl({
370
+ type: "error",
371
+ error: `protocol parse error: ${err.message}`
372
+ });
373
+ return;
374
+ }
375
+ void handleInbound(parsed, ws);
376
+ });
377
+ ws.on("close", () => {
378
+ if (activeSocket === ws) {
379
+ activeSocket = void 0;
380
+ }
381
+ });
382
+ ws.on("error", () => {
383
+ });
384
+ });
385
+ process.on("uncaughtException", (err) => {
386
+ emit({ type: "error", error: serialiseError(err) });
387
+ });
388
+ process.on("unhandledRejection", (err) => {
389
+ emit({ type: "error", error: serialiseError(err) });
390
+ });
391
+ await new Promise((resolve, reject) => {
392
+ if (wss.address() != null) {
393
+ resolve();
394
+ return;
395
+ }
396
+ wss.once("listening", resolve);
397
+ wss.once("error", reject);
398
+ });
399
+ return {
400
+ port: currentBoundPort,
401
+ close: () => new Promise((resolve) => {
402
+ wss.close(() => resolve());
403
+ })
404
+ };
405
+ }
406
+ function serialiseError(err) {
407
+ if (err instanceof Error) {
408
+ return { name: err.name, message: err.message, stack: err.stack };
409
+ }
410
+ return err;
411
+ }
412
+
413
+ // src/bridge/index.ts
414
+ import { randomUUID } from "crypto";
415
+ import { writeFile as writeFile2 } from "fs/promises";
416
+ import { createServer } from "http";
417
+
418
+ // src/bridge/cli-relay.ts
419
+ var CLI_SHIM_FILENAME = "harness-tool.mjs";
420
+ function buildCliShimScript({
421
+ relayPort,
422
+ relayToken
423
+ }) {
424
+ return `#!/usr/bin/env node
425
+ const [toolName, inputJson = '{}'] = process.argv.slice(2);
426
+ if (!toolName) {
427
+ console.error('Usage: harness-tool <tool_name> <json_input>');
428
+ process.exit(64);
429
+ }
430
+ let input;
431
+ try {
432
+ input = JSON.parse(inputJson);
433
+ } catch (error) {
434
+ console.error('Invalid JSON input: ' + (error instanceof Error ? error.message : String(error)));
435
+ process.exit(64);
436
+ }
437
+ const requestId = 'cli-' + Date.now() + '-' + Math.random().toString(16).slice(2);
438
+ const response = await fetch('http://127.0.0.1:${relayPort}', {
439
+ method: 'POST',
440
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ${relayToken}' },
441
+ body: JSON.stringify({ requestId, toolName, input }),
442
+ });
443
+ const text = await response.text();
444
+ let payload;
445
+ try {
446
+ payload = text ? JSON.parse(text) : {};
447
+ } catch {
448
+ payload = { error: text };
449
+ }
450
+ if (!response.ok || payload.error) {
451
+ console.error(String(payload.error ?? ('tool relay failed with HTTP ' + response.status)));
452
+ process.exit(1);
453
+ }
454
+ console.log(JSON.stringify(payload.result ?? payload, null, 2));
455
+ `;
456
+ }
457
+ function composeToolUsageInstructions({
458
+ tools,
459
+ cliShimPath
460
+ }) {
461
+ const lines = [
462
+ "## Host tools",
463
+ "",
464
+ "You have access to the following host-provided tools. To use one, run the following command via your built-in `bash` tool:",
465
+ "",
466
+ ` node ${cliShimPath} <toolName> '<jsonInput>'`,
467
+ "",
468
+ "The script prints the JSON result to stdout. Do not invent another way to call these tools \u2014 only this CLI invocation will work. Pass the JSON input as a single-quoted argument.",
469
+ "For every user request that depends on a host-provided tool, run a separate CLI invocation for each needed tool call in the current turn before answering. Do not reuse previous tool results, and do not say you used a host tool unless the command has completed in the current turn.",
470
+ ""
471
+ ];
472
+ for (const tool of tools) {
473
+ lines.push(`### ${tool.name}`);
474
+ if (tool.description) lines.push(tool.description);
475
+ lines.push(
476
+ `Input schema: \`${JSON.stringify(tool.inputSchema ?? {})}\``,
477
+ ""
478
+ );
479
+ }
480
+ return lines.join("\n");
481
+ }
482
+ function isToolRelayCommand({
483
+ command,
484
+ cliShimPath
485
+ }) {
486
+ return command.includes(cliShimPath);
487
+ }
488
+
489
+ // src/bridge/index.ts
490
+ import { argv, env as procEnv2, stdout as stdout2 } from "process";
491
+ import * as codexSdkModule from "@openai/codex-sdk";
492
+ var NATIVE_TO_COMMON = {
493
+ shell: "bash",
494
+ web_search: "webSearch"
495
+ };
496
+ function toCommonName(nativeName) {
497
+ return NATIVE_TO_COMMON[nativeName] ?? nativeName;
498
+ }
499
+ var args = parseArgs(argv.slice(2));
500
+ var workdir = args.workdir;
501
+ var bridgeStateDir = args.bridgeStateDir;
502
+ if (!workdir) {
503
+ emitFatal("Missing --workdir argument.");
504
+ }
505
+ if (!bridgeStateDir) {
506
+ emitFatal("Missing --bridge-state-dir argument.");
507
+ }
508
+ var bootstrapDir = args.bootstrapDir ?? workdir;
509
+ var codexSdk = codexSdkModule;
510
+ var threadState = { id: void 0 };
511
+ await runBridge({
512
+ bridgeType: "codex",
513
+ bridgeStateDir,
514
+ onStart: runTurn,
515
+ onDetach: () => threadState.id ? { threadId: threadState.id } : {}
516
+ });
517
+ async function runTurn(start, turn) {
518
+ const emit = (msg) => turn.emit(msg);
519
+ if (typeof start.resumeThreadId === "string" && start.resumeThreadId.length > 0) {
520
+ threadState.id = start.resumeThreadId;
521
+ }
522
+ const mcpServers = {};
523
+ let relay;
524
+ let cliShimPath;
525
+ if (start.tools && start.tools.length > 0) {
526
+ const relayToken = randomUUID();
527
+ relay = await startToolRelay({
528
+ relayToken,
529
+ tools: start.tools,
530
+ emit,
531
+ requestToolResult: turn.requestToolResult
532
+ });
533
+ mcpServers["harness-tools"] = {
534
+ enabled: true,
535
+ command: "node",
536
+ args: [`${bootstrapDir}/host-tool-mcp.mjs`],
537
+ env: {
538
+ TOOL_SCHEMAS: JSON.stringify(
539
+ start.tools.map((t) => ({
540
+ name: t.name,
541
+ description: t.description,
542
+ inputSchema: t.inputSchema
543
+ }))
544
+ ),
545
+ TOOL_RELAY_URL: `http://127.0.0.1:${relay.port}`,
546
+ TOOL_RELAY_TOKEN: relayToken
547
+ }
548
+ };
549
+ cliShimPath = `${workdir}/${CLI_SHIM_FILENAME}`;
550
+ await writeFile2(
551
+ cliShimPath,
552
+ buildCliShimScript({ relayPort: relay.port, relayToken }),
553
+ "utf8"
554
+ );
555
+ }
556
+ const codexConfig = {};
557
+ if (Object.keys(mcpServers).length > 0) codexConfig.mcp_servers = mcpServers;
558
+ const apiBaseUrl = procEnv2.AI_GATEWAY_API_KEY ? procEnv2.AI_GATEWAY_BASE_URL || "https://ai-gateway.vercel.sh/v1" : procEnv2.OPENAI_BASE_URL;
559
+ if (apiBaseUrl) {
560
+ codexConfig.preferred_auth_method = "apikey";
561
+ codexConfig.model_provider = "agent_bridge_openai";
562
+ codexConfig.model_providers = {
563
+ agent_bridge_openai: {
564
+ name: procEnv2.CODEX_MODEL_PROVIDER_NAME || "Agent Bridge OpenAI",
565
+ base_url: apiBaseUrl,
566
+ env_key: "CODEX_API_KEY",
567
+ wire_api: "responses",
568
+ supports_websockets: false
569
+ }
570
+ };
571
+ }
572
+ const usesConfiguredModelProvider = typeof codexConfig.model_provider === "string";
573
+ const codex = new codexSdk.Codex({
574
+ ...procEnv2.CODEX_API_KEY ? { apiKey: procEnv2.CODEX_API_KEY } : {},
575
+ ...!usesConfiguredModelProvider && apiBaseUrl ? { baseUrl: apiBaseUrl } : {},
576
+ env: Object.fromEntries(
577
+ Object.entries(procEnv2).filter(
578
+ (entry) => typeof entry[1] === "string"
579
+ )
580
+ ),
581
+ ...Object.keys(codexConfig).length > 0 ? { config: codexConfig } : {}
582
+ });
583
+ const threadOptions = {
584
+ ...start.model ? { model: start.model } : {},
585
+ sandboxMode: "danger-full-access",
586
+ approvalPolicy: "never",
587
+ workingDirectory: workdir,
588
+ skipGitRepoCheck: true,
589
+ ...start.reasoningEffort ? { modelReasoningEffort: start.reasoningEffort } : {},
590
+ webSearchMode: start.webSearch ? "live" : "disabled"
591
+ };
592
+ const thread = threadState.id ? codex.resumeThread(threadState.id, threadOptions) : codex.startThread(threadOptions);
593
+ emit({ type: "stream-start" });
594
+ const userMessage = composeUserMessage({
595
+ text: start.prompt,
596
+ instructions: start.instructions,
597
+ // Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
598
+ toolUsageBlock: cliShimPath && start.tools && start.tools.length > 0 ? composeToolUsageInstructions({
599
+ tools: start.tools,
600
+ cliShimPath
601
+ }) : void 0
602
+ });
603
+ let turnUsage;
604
+ const textByItem = /* @__PURE__ */ new Map();
605
+ const reasoningByItem = /* @__PURE__ */ new Map();
606
+ try {
607
+ const { events } = await thread.runStreamed(userMessage, {
608
+ signal: turn.abortSignal
609
+ });
610
+ for await (const event of events) {
611
+ if (turn.abortSignal.aborted) break;
612
+ if (event.type === "thread.started" && typeof event.thread_id === "string") {
613
+ threadState.id = event.thread_id;
614
+ emit({ type: "bridge-thread", threadId: event.thread_id });
615
+ }
616
+ if (cliShimPath && event.item?.type === "command_execution" && typeof event.item.command === "string" && isToolRelayCommand({ command: event.item.command, cliShimPath })) {
617
+ continue;
618
+ }
619
+ translateAndEmit(event, {
620
+ send: emit,
621
+ textByItem,
622
+ reasoningByItem,
623
+ setTurnUsage: (u) => turnUsage = u
624
+ });
625
+ }
626
+ } catch (err) {
627
+ emit({ type: "error", error: serialiseError2(err) });
628
+ return;
629
+ } finally {
630
+ relay?.close();
631
+ }
632
+ emit({
633
+ type: "finish",
634
+ finishReason: { unified: "stop", raw: "stop" },
635
+ totalUsage: turnUsage ?? defaultUsage()
636
+ });
637
+ void turn.pendingUserMessages;
638
+ }
639
+ function extractMcpToolCallResult(item) {
640
+ if (item.result === void 0 || item.result === null || typeof item.result !== "object") {
641
+ return item.error?.message ? { error: item.error.message } : null;
642
+ }
643
+ const result = item.result;
644
+ if (result.structured_content !== void 0 && result.structured_content !== null) {
645
+ return result.structured_content;
646
+ }
647
+ return result.content ?? null;
648
+ }
649
+ function translateAndEmit(event, ctx) {
650
+ if (event.type === "turn.completed") {
651
+ if (event.usage) ctx.setTurnUsage(mapUsage(event.usage));
652
+ ctx.send({
653
+ type: "finish-step",
654
+ finishReason: { unified: "stop", raw: "stop" },
655
+ usage: event.usage ? mapUsage(event.usage) : defaultUsage()
656
+ });
657
+ return;
658
+ }
659
+ if (event.type === "turn.failed") {
660
+ ctx.send({
661
+ type: "error",
662
+ error: event.error?.message ?? "codex turn failed"
663
+ });
664
+ return;
665
+ }
666
+ if (event.type === "error") {
667
+ ctx.send({ type: "error", error: event.message ?? "codex error" });
668
+ return;
669
+ }
670
+ if (!event.item) return;
671
+ const item = event.item;
672
+ const id = item.id ?? randomUUID();
673
+ if (item.type === "agent_message" && typeof item.text === "string") {
674
+ if (!ctx.textByItem.has(id)) {
675
+ ctx.send({ type: "text-start", id });
676
+ ctx.textByItem.set(id, "");
677
+ }
678
+ const last = ctx.textByItem.get(id) ?? "";
679
+ const next = item.text;
680
+ if (next.length > last.length) {
681
+ ctx.send({ type: "text-delta", id, delta: next.slice(last.length) });
682
+ ctx.textByItem.set(id, next);
683
+ }
684
+ if (event.type === "item.completed") ctx.send({ type: "text-end", id });
685
+ return;
686
+ }
687
+ if (item.type === "reasoning" && typeof item.text === "string") {
688
+ if (!ctx.reasoningByItem.has(id)) {
689
+ ctx.send({ type: "reasoning-start", id });
690
+ ctx.reasoningByItem.set(id, "");
691
+ }
692
+ const last = ctx.reasoningByItem.get(id) ?? "";
693
+ const next = item.text;
694
+ if (next.length > last.length) {
695
+ ctx.send({ type: "reasoning-delta", id, delta: next.slice(last.length) });
696
+ ctx.reasoningByItem.set(id, next);
697
+ }
698
+ if (event.type === "item.completed")
699
+ ctx.send({ type: "reasoning-end", id });
700
+ return;
701
+ }
702
+ if (item.type === "command_execution") {
703
+ const nativeName = "shell";
704
+ if (event.type === "item.started") {
705
+ ctx.send({
706
+ type: "tool-call",
707
+ toolCallId: id,
708
+ toolName: toCommonName(nativeName),
709
+ nativeName,
710
+ input: JSON.stringify({ command: item.command ?? "" }),
711
+ providerExecuted: true
712
+ });
713
+ } else if (event.type === "item.completed") {
714
+ ctx.send({
715
+ type: "tool-result",
716
+ toolCallId: id,
717
+ toolName: toCommonName(nativeName),
718
+ result: {
719
+ exitCode: item.exit_code ?? null,
720
+ output: item.aggregated_output ?? "",
721
+ status: item.status ?? "completed"
722
+ }
723
+ });
724
+ }
725
+ return;
726
+ }
727
+ if (item.type === "mcp_tool_call") {
728
+ const isHostTool = item.server === "harness-tools";
729
+ if (event.type === "item.started") {
730
+ ctx.send({
731
+ type: "tool-call",
732
+ toolCallId: id,
733
+ toolName: item.tool ?? "unknown",
734
+ ...isHostTool ? {} : { nativeName: item.tool ?? "unknown" },
735
+ input: JSON.stringify(item.arguments ?? {}),
736
+ providerExecuted: !isHostTool
737
+ });
738
+ } else if (event.type === "item.completed") {
739
+ ctx.send({
740
+ type: "tool-result",
741
+ toolCallId: id,
742
+ toolName: item.tool ?? "unknown",
743
+ result: extractMcpToolCallResult(item)
744
+ });
745
+ }
746
+ return;
747
+ }
748
+ if (item.type === "web_search") {
749
+ const nativeName = "web_search";
750
+ if (event.type === "item.started") {
751
+ ctx.send({
752
+ type: "tool-call",
753
+ toolCallId: id,
754
+ toolName: toCommonName(nativeName),
755
+ nativeName,
756
+ input: JSON.stringify({ query: item.query ?? "" }),
757
+ providerExecuted: true
758
+ });
759
+ } else if (event.type === "item.completed") {
760
+ ctx.send({
761
+ type: "tool-result",
762
+ toolCallId: id,
763
+ toolName: toCommonName(nativeName),
764
+ result: item.result ?? null
765
+ });
766
+ }
767
+ return;
768
+ }
769
+ if (item.type === "file_change" && event.type === "item.completed") {
770
+ for (const change of item.changes ?? []) {
771
+ ctx.send({
772
+ type: "file-change",
773
+ event: change.kind === "add" ? "create" : change.kind === "delete" ? "delete" : "modify",
774
+ path: change.path
775
+ });
776
+ }
777
+ return;
778
+ }
779
+ if (item.type === "error" && event.type === "item.completed") {
780
+ ctx.send({
781
+ type: "error",
782
+ error: item.message ?? "codex item error"
783
+ });
784
+ return;
785
+ }
786
+ }
787
+ function mapUsage(usage) {
788
+ const input = usage.input_tokens ?? 0;
789
+ const cacheRead = usage.cached_input_tokens ?? 0;
790
+ return {
791
+ inputTokens: {
792
+ total: input,
793
+ noCache: Math.max(0, input - cacheRead),
794
+ cacheRead,
795
+ cacheWrite: 0
796
+ },
797
+ outputTokens: {
798
+ total: usage.output_tokens ?? 0,
799
+ text: usage.output_tokens ?? 0
800
+ }
801
+ };
802
+ }
803
+ function defaultUsage() {
804
+ return {
805
+ inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
806
+ outputTokens: { total: 0, text: 0 }
807
+ };
808
+ }
809
+ function composeUserMessage({
810
+ text,
811
+ instructions,
812
+ toolUsageBlock
813
+ }) {
814
+ const blocks = [];
815
+ if (instructions) {
816
+ blocks.push(
817
+ `<session-instructions>
818
+ The block below is operating guidance from the system, not a message from the user \u2014 follow it, but do not mention it or attribute it to the user.
819
+
820
+ ${instructions}
821
+ </session-instructions>`
822
+ );
823
+ }
824
+ if (toolUsageBlock) blocks.push(toolUsageBlock);
825
+ blocks.push(instructions ? `<user-message>
826
+ ${text}
827
+ </user-message>` : text);
828
+ return blocks.join("\n\n");
829
+ }
830
+ async function startToolRelay({
831
+ relayToken,
832
+ tools,
833
+ emit,
834
+ requestToolResult
835
+ }) {
836
+ const toolNames = new Set(tools.map((t) => t.name));
837
+ const server = createServer(async (req, res) => {
838
+ try {
839
+ if (req.method !== "POST" || req.url !== "/" || req.headers.authorization !== `Bearer ${relayToken}`) {
840
+ res.writeHead(401, { "Content-Type": "application/json" });
841
+ res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
842
+ return;
843
+ }
844
+ const chunks = [];
845
+ for await (const chunk of req) {
846
+ chunks.push(chunk);
847
+ }
848
+ const body = Buffer.concat(chunks).toString("utf8");
849
+ const { requestId, toolName, input } = JSON.parse(body);
850
+ if (!toolNames.has(toolName)) {
851
+ res.writeHead(403, { "Content-Type": "application/json" });
852
+ res.end(
853
+ JSON.stringify({ error: `Tool "${toolName}" is not available` })
854
+ );
855
+ return;
856
+ }
857
+ emit({
858
+ type: "tool-call",
859
+ toolCallId: requestId,
860
+ toolName,
861
+ input: JSON.stringify(input ?? {}),
862
+ providerExecuted: false
863
+ });
864
+ const { output, isError } = await requestToolResult(requestId);
865
+ emit({
866
+ type: "tool-result",
867
+ toolCallId: requestId,
868
+ toolName,
869
+ result: output ?? null,
870
+ isError: !!isError
871
+ });
872
+ res.writeHead(200, { "Content-Type": "application/json" });
873
+ res.end(JSON.stringify({ result: output }));
874
+ } catch (error) {
875
+ res.writeHead(500, { "Content-Type": "application/json" });
876
+ res.end(
877
+ JSON.stringify({
878
+ error: error instanceof Error ? error.message : String(error)
879
+ })
880
+ );
881
+ }
882
+ });
883
+ await new Promise(
884
+ (resolve) => server.listen(0, "127.0.0.1", () => resolve())
885
+ );
886
+ const address = server.address();
887
+ if (!address || typeof address === "string") {
888
+ throw new Error("tool relay did not expose a numeric port");
889
+ }
890
+ return {
891
+ port: address.port,
892
+ close: () => closeServer(server)
893
+ };
894
+ }
895
+ function closeServer(server) {
896
+ try {
897
+ server.close();
898
+ } catch {
899
+ }
900
+ }
901
+ function parseArgs(args2) {
902
+ const out = {};
903
+ for (let i = 0; i < args2.length; i++) {
904
+ if (args2[i] === "--workdir" && i + 1 < args2.length) {
905
+ out.workdir = args2[++i];
906
+ } else if (args2[i] === "--bridge-state-dir" && i + 1 < args2.length) {
907
+ out.bridgeStateDir = args2[++i];
908
+ } else if (args2[i] === "--bootstrap-dir" && i + 1 < args2.length) {
909
+ out.bootstrapDir = args2[++i];
910
+ }
911
+ }
912
+ return out;
913
+ }
914
+ function serialiseError2(err) {
915
+ if (err instanceof Error) {
916
+ return { name: err.name, message: err.message, stack: err.stack };
917
+ }
918
+ return err;
919
+ }
920
+ function emitFatal(message) {
921
+ stdout2.write(JSON.stringify({ type: "bridge-fatal", message }) + "\n");
922
+ process.exit(1);
923
+ }
924
+ //# sourceMappingURL=index.mjs.map