@ai-sdk/harness-opencode 0.0.0 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +13 -0
- package/README.md +6 -0
- package/dist/bridge/host-tool-mcp.mjs +103 -0
- package/dist/bridge/host-tool-mcp.mjs.map +1 -0
- package/dist/bridge/index.mjs +1984 -0
- package/dist/bridge/index.mjs.map +1 -0
- package/dist/bridge/package.json +13 -0
- package/dist/bridge/pnpm-lock.yaml +933 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.js +1051 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -1
- package/src/bridge/host-tool-mcp.ts +136 -0
- package/src/bridge/index.ts +1846 -0
- package/src/bridge/opencode-events.ts +88 -0
- package/src/bridge/opencode-path.ts +17 -0
- package/src/bridge/opencode-usage.ts +156 -0
- package/src/bridge/package.json +13 -0
- package/src/bridge/pnpm-lock.yaml +933 -0
- package/src/index.ts +11 -0
- package/src/opencode-auth.ts +175 -0
- package/src/opencode-bridge-protocol.ts +29 -0
- package/src/opencode-harness.ts +1103 -0
|
@@ -0,0 +1,1984 @@
|
|
|
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 { mkdirSync } from "fs";
|
|
416
|
+
import { createServer } from "http";
|
|
417
|
+
import path2 from "path";
|
|
418
|
+
import { argv, env as procEnv2 } from "process";
|
|
419
|
+
import {
|
|
420
|
+
createOpencodeClient,
|
|
421
|
+
createOpencodeServer
|
|
422
|
+
} from "@opencode-ai/sdk/v2";
|
|
423
|
+
|
|
424
|
+
// src/bridge/opencode-events.ts
|
|
425
|
+
function unwrapOpenCodeEvent(rawEvent) {
|
|
426
|
+
if (!rawEvent || typeof rawEvent !== "object") return void 0;
|
|
427
|
+
const raw = rawEvent;
|
|
428
|
+
if (raw.type === "sync" && raw.syncEvent) {
|
|
429
|
+
const sync = raw.syncEvent;
|
|
430
|
+
return {
|
|
431
|
+
id: String(sync.id ?? raw.id ?? ""),
|
|
432
|
+
type: stripSyncVersion(String(sync.type ?? "")),
|
|
433
|
+
properties: asRecord(sync.data) ?? {}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
id: typeof raw.id === "string" ? raw.id : void 0,
|
|
438
|
+
type: typeof raw.type === "string" ? stripSyncVersion(raw.type) : void 0,
|
|
439
|
+
properties: asRecord(raw.properties) ?? asRecord(raw.data) ?? {}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function getOpenCodeEventSessionId(event) {
|
|
443
|
+
const props = event.properties;
|
|
444
|
+
if (!props) return void 0;
|
|
445
|
+
if (typeof props.sessionID === "string") return props.sessionID;
|
|
446
|
+
if (typeof props.sessionId === "string") return props.sessionId;
|
|
447
|
+
if (event.type?.startsWith("session.") && typeof props.id === "string") {
|
|
448
|
+
return props.id;
|
|
449
|
+
}
|
|
450
|
+
const part = props.part;
|
|
451
|
+
if (part && typeof part === "object" && !Array.isArray(part) && typeof part.sessionID === "string") {
|
|
452
|
+
return part.sessionID;
|
|
453
|
+
}
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
function isStepSettlementEvent(event) {
|
|
457
|
+
return event.type === "session.next.step.ended" || event.type === "session.next.step.failed" || event.type === "session.error";
|
|
458
|
+
}
|
|
459
|
+
function emitMissingFinalDelta({
|
|
460
|
+
id,
|
|
461
|
+
fullText,
|
|
462
|
+
emittedText,
|
|
463
|
+
emit,
|
|
464
|
+
type
|
|
465
|
+
}) {
|
|
466
|
+
if (!fullText || fullText === emittedText || !fullText.startsWith(emittedText)) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
emit({ type, id, delta: fullText.slice(emittedText.length) });
|
|
470
|
+
}
|
|
471
|
+
function stripSyncVersion(type) {
|
|
472
|
+
return type.replace(/\.\d+$/, "");
|
|
473
|
+
}
|
|
474
|
+
function asRecord(value) {
|
|
475
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
476
|
+
return void 0;
|
|
477
|
+
return value;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/bridge/opencode-path.ts
|
|
481
|
+
import path from "path";
|
|
482
|
+
var fallbackPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
483
|
+
function prependOpenCodeBinToPath({
|
|
484
|
+
bootstrapDir: bootstrapDir2,
|
|
485
|
+
env
|
|
486
|
+
}) {
|
|
487
|
+
env.PATH = [
|
|
488
|
+
path.join(bootstrapDir2, "node_modules", ".bin"),
|
|
489
|
+
env.PATH || fallbackPath
|
|
490
|
+
].join(path.delimiter);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/bridge/opencode-usage.ts
|
|
494
|
+
function mapUsage(tokens) {
|
|
495
|
+
const value = extractOpenCodeTokens(tokens) ?? zeroOpenCodeTokens();
|
|
496
|
+
const cacheRead = value.cache.read;
|
|
497
|
+
return {
|
|
498
|
+
inputTokens: {
|
|
499
|
+
total: value.input,
|
|
500
|
+
noCache: Math.max(0, value.input - cacheRead),
|
|
501
|
+
cacheRead,
|
|
502
|
+
cacheWrite: value.cache.write
|
|
503
|
+
},
|
|
504
|
+
outputTokens: {
|
|
505
|
+
total: value.output + value.reasoning,
|
|
506
|
+
text: value.output,
|
|
507
|
+
reasoning: value.reasoning
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function defaultUsage() {
|
|
512
|
+
return mapUsage(zeroOpenCodeTokens());
|
|
513
|
+
}
|
|
514
|
+
function extractSessionTokens(value) {
|
|
515
|
+
const record = asRecord2(value);
|
|
516
|
+
if (!record) return void 0;
|
|
517
|
+
const tokens = extractOpenCodeTokens(record.tokens) ?? extractOpenCodeTokens(asRecord2(record.info)?.tokens) ?? extractOpenCodeTokens(asRecord2(record.data)?.tokens) ?? extractOpenCodeTokens(asRecord2(asRecord2(record.data)?.data)?.tokens);
|
|
518
|
+
return tokens;
|
|
519
|
+
}
|
|
520
|
+
function subtractSessionTokens({
|
|
521
|
+
before,
|
|
522
|
+
after
|
|
523
|
+
}) {
|
|
524
|
+
return {
|
|
525
|
+
input: diff({ before: before.input, after: after.input }),
|
|
526
|
+
output: diff({ before: before.output, after: after.output }),
|
|
527
|
+
reasoning: diff({ before: before.reasoning, after: after.reasoning }),
|
|
528
|
+
cache: {
|
|
529
|
+
read: diff({ before: before.cache.read, after: after.cache.read }),
|
|
530
|
+
write: diff({ before: before.cache.write, after: after.cache.write })
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function addUsage({
|
|
535
|
+
left,
|
|
536
|
+
right
|
|
537
|
+
}) {
|
|
538
|
+
if (left == null) return right;
|
|
539
|
+
const leftInput = asTokenGroup(left.inputTokens);
|
|
540
|
+
const rightInput = asTokenGroup(right.inputTokens);
|
|
541
|
+
const leftOutput = asTokenGroup(left.outputTokens);
|
|
542
|
+
const rightOutput = asTokenGroup(right.outputTokens);
|
|
543
|
+
return {
|
|
544
|
+
inputTokens: {
|
|
545
|
+
total: add({ left: leftInput.total, right: rightInput.total }),
|
|
546
|
+
noCache: add({ left: leftInput.noCache, right: rightInput.noCache }),
|
|
547
|
+
cacheRead: add({
|
|
548
|
+
left: leftInput.cacheRead,
|
|
549
|
+
right: rightInput.cacheRead
|
|
550
|
+
}),
|
|
551
|
+
cacheWrite: add({
|
|
552
|
+
left: leftInput.cacheWrite,
|
|
553
|
+
right: rightInput.cacheWrite
|
|
554
|
+
})
|
|
555
|
+
},
|
|
556
|
+
outputTokens: {
|
|
557
|
+
total: add({ left: leftOutput.total, right: rightOutput.total }),
|
|
558
|
+
text: add({ left: leftOutput.text, right: rightOutput.text }),
|
|
559
|
+
reasoning: add({
|
|
560
|
+
left: leftOutput.reasoning,
|
|
561
|
+
right: rightOutput.reasoning
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function extractOpenCodeTokens(value) {
|
|
567
|
+
const record = asRecord2(value);
|
|
568
|
+
const cache = asRecord2(record?.cache);
|
|
569
|
+
if (!record || !cache) return void 0;
|
|
570
|
+
return {
|
|
571
|
+
input: numberValue(record.input),
|
|
572
|
+
output: numberValue(record.output),
|
|
573
|
+
reasoning: numberValue(record.reasoning),
|
|
574
|
+
cache: {
|
|
575
|
+
read: numberValue(cache.read),
|
|
576
|
+
write: numberValue(cache.write)
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function zeroOpenCodeTokens() {
|
|
581
|
+
return {
|
|
582
|
+
input: 0,
|
|
583
|
+
output: 0,
|
|
584
|
+
reasoning: 0,
|
|
585
|
+
cache: { read: 0, write: 0 }
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function asTokenGroup(value) {
|
|
589
|
+
return asRecord2(value) ?? {};
|
|
590
|
+
}
|
|
591
|
+
function asRecord2(value) {
|
|
592
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
593
|
+
return void 0;
|
|
594
|
+
return value;
|
|
595
|
+
}
|
|
596
|
+
function numberValue(value) {
|
|
597
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
598
|
+
}
|
|
599
|
+
function diff({ before, after }) {
|
|
600
|
+
return Math.max(0, after - before);
|
|
601
|
+
}
|
|
602
|
+
function add({
|
|
603
|
+
left,
|
|
604
|
+
right
|
|
605
|
+
}) {
|
|
606
|
+
const leftNumber = typeof left === "number" ? left : void 0;
|
|
607
|
+
const rightNumber = typeof right === "number" ? right : void 0;
|
|
608
|
+
return leftNumber == null && rightNumber == null ? void 0 : (leftNumber ?? 0) + (rightNumber ?? 0);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/bridge/index.ts
|
|
612
|
+
var NATIVE_TO_COMMON = {
|
|
613
|
+
view: "read",
|
|
614
|
+
read: "read",
|
|
615
|
+
write: "write",
|
|
616
|
+
edit: "edit",
|
|
617
|
+
bash: "bash",
|
|
618
|
+
glob: "glob",
|
|
619
|
+
grep: "grep"
|
|
620
|
+
};
|
|
621
|
+
var OPENCODE_TO_WIRE = {
|
|
622
|
+
list: "ls",
|
|
623
|
+
ls: "ls",
|
|
624
|
+
webfetch: "webfetch",
|
|
625
|
+
task: "agent",
|
|
626
|
+
agent: "agent",
|
|
627
|
+
subtask: "agent"
|
|
628
|
+
};
|
|
629
|
+
var TOOL_KIND = {
|
|
630
|
+
read: "readonly",
|
|
631
|
+
glob: "readonly",
|
|
632
|
+
grep: "readonly",
|
|
633
|
+
ls: "readonly",
|
|
634
|
+
webfetch: "readonly",
|
|
635
|
+
write: "edit",
|
|
636
|
+
edit: "edit",
|
|
637
|
+
bash: "bash",
|
|
638
|
+
agent: "bash",
|
|
639
|
+
skill: "edit",
|
|
640
|
+
todowrite: "edit"
|
|
641
|
+
};
|
|
642
|
+
var args = parseArgs(argv.slice(2));
|
|
643
|
+
var workdir = args.workdir ?? emitFatal("Missing --workdir argument.");
|
|
644
|
+
var bridgeStateDir = args.bridgeStateDir ?? emitFatal("Missing --bridge-state-dir argument.");
|
|
645
|
+
var bootstrapDir = args.bootstrapDir ?? workdir;
|
|
646
|
+
var skillsDir = args.skillsDir;
|
|
647
|
+
var runtime = { toolNames: /* @__PURE__ */ new Set() };
|
|
648
|
+
prependOpenCodeBinToPath({ bootstrapDir, env: procEnv2 });
|
|
649
|
+
mkdirSync(process.env.HOME ?? "/tmp/opencode-home", { recursive: true });
|
|
650
|
+
await runBridge({
|
|
651
|
+
bridgeType: "opencode",
|
|
652
|
+
bridgeStateDir,
|
|
653
|
+
onStart: runTurn,
|
|
654
|
+
onDetach: () => runtime.sessionId ? { openCodeSessionId: runtime.sessionId } : {}
|
|
655
|
+
});
|
|
656
|
+
async function runTurn(start, turn) {
|
|
657
|
+
const emit = (msg) => turn.emit(msg);
|
|
658
|
+
let totalUsage;
|
|
659
|
+
try {
|
|
660
|
+
await ensureRuntime({ start, turn, emit });
|
|
661
|
+
const client = runtime.client;
|
|
662
|
+
const sessionId = await ensureSession({ client, start, emit });
|
|
663
|
+
if (start.operation === "compact") {
|
|
664
|
+
await runCompaction({ client, sessionId, start, turn, emit });
|
|
665
|
+
} else {
|
|
666
|
+
totalUsage = await runPrompt({ client, sessionId, start, turn, emit });
|
|
667
|
+
}
|
|
668
|
+
} catch (err) {
|
|
669
|
+
emit({ type: "error", error: serialiseError2(err) });
|
|
670
|
+
} finally {
|
|
671
|
+
emit({
|
|
672
|
+
type: "finish",
|
|
673
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
674
|
+
totalUsage: totalUsage ?? defaultUsage()
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async function ensureRuntime({
|
|
679
|
+
start,
|
|
680
|
+
turn,
|
|
681
|
+
emit
|
|
682
|
+
}) {
|
|
683
|
+
if (runtime.client) return;
|
|
684
|
+
let relayToken;
|
|
685
|
+
if (start.tools && start.tools.length > 0) {
|
|
686
|
+
relayToken = randomUUID();
|
|
687
|
+
runtime.toolNames = new Set(start.tools.map((tool) => tool.name));
|
|
688
|
+
runtime.relay = await startToolRelay({
|
|
689
|
+
relayToken,
|
|
690
|
+
tools: start.tools,
|
|
691
|
+
emit,
|
|
692
|
+
requestToolResult: turn.requestToolResult
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
const server = await createOpencodeServer({
|
|
696
|
+
hostname: "127.0.0.1",
|
|
697
|
+
port: 0,
|
|
698
|
+
timeout: 3e4,
|
|
699
|
+
config: buildOpenCodeConfig({
|
|
700
|
+
start,
|
|
701
|
+
relayToken,
|
|
702
|
+
relayPort: runtime.relay?.port
|
|
703
|
+
})
|
|
704
|
+
});
|
|
705
|
+
runtime.server = server;
|
|
706
|
+
runtime.client = createOpencodeClient({
|
|
707
|
+
baseUrl: server.url,
|
|
708
|
+
directory: workdir
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function buildOpenCodeConfig({
|
|
712
|
+
start,
|
|
713
|
+
relayToken,
|
|
714
|
+
relayPort
|
|
715
|
+
}) {
|
|
716
|
+
const config = {
|
|
717
|
+
share: "disabled",
|
|
718
|
+
autoupdate: false,
|
|
719
|
+
permission: {
|
|
720
|
+
read: "allow",
|
|
721
|
+
glob: "allow",
|
|
722
|
+
grep: "allow",
|
|
723
|
+
list: "allow",
|
|
724
|
+
edit: "ask",
|
|
725
|
+
bash: "ask",
|
|
726
|
+
external_directory: "ask",
|
|
727
|
+
webfetch: "ask",
|
|
728
|
+
doom_loop: "ask",
|
|
729
|
+
task: "ask"
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
if (start.model) config.model = start.model;
|
|
733
|
+
if (skillsDir) config.skills = { paths: [skillsDir] };
|
|
734
|
+
const provider = buildProviderConfig(start);
|
|
735
|
+
if (provider) config.provider = provider;
|
|
736
|
+
if (relayToken && relayPort && start.tools && start.tools.length > 0) {
|
|
737
|
+
config.mcp = {
|
|
738
|
+
"harness-tools": {
|
|
739
|
+
type: "local",
|
|
740
|
+
enabled: true,
|
|
741
|
+
command: ["node", `${bootstrapDir}/host-tool-mcp.mjs`],
|
|
742
|
+
environment: {
|
|
743
|
+
TOOL_SCHEMAS: JSON.stringify(
|
|
744
|
+
start.tools.map((t) => ({
|
|
745
|
+
name: t.name,
|
|
746
|
+
description: t.description,
|
|
747
|
+
inputSchema: t.inputSchema
|
|
748
|
+
}))
|
|
749
|
+
),
|
|
750
|
+
TOOL_RELAY_URL: `http://127.0.0.1:${relayPort}`,
|
|
751
|
+
TOOL_RELAY_TOKEN: relayToken
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
return config;
|
|
757
|
+
}
|
|
758
|
+
function buildProviderConfig(start) {
|
|
759
|
+
const model = splitModel(start.model, start.provider);
|
|
760
|
+
const providerID = model.providerID ?? start.provider ?? procEnv2.OPENAI_NAME ?? "anthropic";
|
|
761
|
+
const modelID = model.modelID;
|
|
762
|
+
if (procEnv2.AI_GATEWAY_API_KEY && procEnv2.AI_GATEWAY_BASE_URL) {
|
|
763
|
+
return {
|
|
764
|
+
[providerID]: {
|
|
765
|
+
options: {
|
|
766
|
+
apiKey: procEnv2.AI_GATEWAY_API_KEY,
|
|
767
|
+
baseURL: toOpenCodeGatewayBaseUrl(procEnv2.AI_GATEWAY_BASE_URL)
|
|
768
|
+
},
|
|
769
|
+
...modelID ? {
|
|
770
|
+
models: {
|
|
771
|
+
[modelID]: { id: modelID, name: modelID }
|
|
772
|
+
}
|
|
773
|
+
} : {}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if ((procEnv2.OPENAI_NAME || providerID !== "anthropic" && providerID !== "openai") && (procEnv2.OPENAI_API_KEY || procEnv2.OPENAI_BASE_URL)) {
|
|
778
|
+
const openAICompatibleProviderID = procEnv2.OPENAI_NAME ?? providerID;
|
|
779
|
+
return {
|
|
780
|
+
[openAICompatibleProviderID]: {
|
|
781
|
+
options: {
|
|
782
|
+
...procEnv2.OPENAI_API_KEY ? { apiKey: procEnv2.OPENAI_API_KEY } : {},
|
|
783
|
+
...procEnv2.OPENAI_BASE_URL ? { baseURL: procEnv2.OPENAI_BASE_URL } : {},
|
|
784
|
+
...parseOpenAIQueryParams()
|
|
785
|
+
},
|
|
786
|
+
...modelID ? {
|
|
787
|
+
models: {
|
|
788
|
+
[modelID]: { id: modelID, name: modelID }
|
|
789
|
+
}
|
|
790
|
+
} : {}
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
if (providerID === "anthropic" && (procEnv2.ANTHROPIC_API_KEY || procEnv2.ANTHROPIC_AUTH_TOKEN || procEnv2.ANTHROPIC_BASE_URL)) {
|
|
795
|
+
return {
|
|
796
|
+
anthropic: {
|
|
797
|
+
options: {
|
|
798
|
+
...procEnv2.ANTHROPIC_API_KEY ? { apiKey: procEnv2.ANTHROPIC_API_KEY } : {},
|
|
799
|
+
...procEnv2.ANTHROPIC_AUTH_TOKEN ? { authToken: procEnv2.ANTHROPIC_AUTH_TOKEN } : {},
|
|
800
|
+
...procEnv2.ANTHROPIC_BASE_URL ? { baseURL: procEnv2.ANTHROPIC_BASE_URL } : {}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
if (providerID === "openai" && (procEnv2.OPENAI_API_KEY || procEnv2.OPENAI_BASE_URL)) {
|
|
806
|
+
return {
|
|
807
|
+
openai: {
|
|
808
|
+
options: {
|
|
809
|
+
...procEnv2.OPENAI_API_KEY ? { apiKey: procEnv2.OPENAI_API_KEY } : {},
|
|
810
|
+
...procEnv2.OPENAI_BASE_URL ? { baseURL: procEnv2.OPENAI_BASE_URL } : {},
|
|
811
|
+
...procEnv2.OPENAI_ORGANIZATION ? { organization: procEnv2.OPENAI_ORGANIZATION } : {},
|
|
812
|
+
...procEnv2.OPENAI_PROJECT ? { project: procEnv2.OPENAI_PROJECT } : {},
|
|
813
|
+
...parseOpenAIQueryParams()
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
function parseOpenAIQueryParams() {
|
|
821
|
+
if (!procEnv2.OPENAI_QUERY_PARAMS_JSON) return {};
|
|
822
|
+
try {
|
|
823
|
+
const parsed = JSON.parse(procEnv2.OPENAI_QUERY_PARAMS_JSON);
|
|
824
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
825
|
+
return { queryParams: parsed };
|
|
826
|
+
}
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
829
|
+
return {};
|
|
830
|
+
}
|
|
831
|
+
function toOpenCodeGatewayBaseUrl(baseUrl) {
|
|
832
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
833
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
834
|
+
}
|
|
835
|
+
async function legacySessionGet({
|
|
836
|
+
client,
|
|
837
|
+
sessionId
|
|
838
|
+
}) {
|
|
839
|
+
const session = client.session;
|
|
840
|
+
if (!session?.get) return client.v2.session.get({ sessionID: sessionId });
|
|
841
|
+
return session.get({ sessionID: sessionId });
|
|
842
|
+
}
|
|
843
|
+
async function legacySessionCreate({
|
|
844
|
+
client
|
|
845
|
+
}) {
|
|
846
|
+
return client.session.create({});
|
|
847
|
+
}
|
|
848
|
+
async function legacySessionPrompt({
|
|
849
|
+
client,
|
|
850
|
+
sessionId,
|
|
851
|
+
start
|
|
852
|
+
}) {
|
|
853
|
+
return client.session.prompt({
|
|
854
|
+
sessionID: sessionId,
|
|
855
|
+
...start.instructions ? { system: start.instructions } : {},
|
|
856
|
+
...start.variant ? { variant: start.variant } : {},
|
|
857
|
+
parts: [{ type: "text", text: start.prompt }]
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
async function legacySessionSummarize({
|
|
861
|
+
client,
|
|
862
|
+
sessionId,
|
|
863
|
+
model
|
|
864
|
+
}) {
|
|
865
|
+
return client.session.summarize({
|
|
866
|
+
sessionID: sessionId,
|
|
867
|
+
auto: false,
|
|
868
|
+
providerID: model.providerID,
|
|
869
|
+
modelID: model.modelID
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
async function subscribeLegacyEvents({
|
|
873
|
+
client,
|
|
874
|
+
signal
|
|
875
|
+
}) {
|
|
876
|
+
const subscribed = await client.event.subscribe(void 0, {
|
|
877
|
+
signal,
|
|
878
|
+
sseMaxRetryAttempts: 0
|
|
879
|
+
});
|
|
880
|
+
return getEventStream(subscribed);
|
|
881
|
+
}
|
|
882
|
+
function readSessionId(data) {
|
|
883
|
+
if (!data || typeof data !== "object") return void 0;
|
|
884
|
+
const record = data;
|
|
885
|
+
if (typeof record.id === "string") return record.id;
|
|
886
|
+
if (typeof record.data?.id === "string") return record.data.id;
|
|
887
|
+
return void 0;
|
|
888
|
+
}
|
|
889
|
+
function isAsyncIterable(value) {
|
|
890
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
891
|
+
}
|
|
892
|
+
function getEventStream(source) {
|
|
893
|
+
if (!source || typeof source !== "object") return null;
|
|
894
|
+
const candidate = source;
|
|
895
|
+
if (isAsyncIterable(candidate.stream)) return candidate.stream;
|
|
896
|
+
if (isAsyncIterable(candidate.data)) return candidate.data;
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
function legacyStatusType(event) {
|
|
900
|
+
const status = event.properties?.status;
|
|
901
|
+
return status && typeof status === "object" ? String(status.type ?? "") : void 0;
|
|
902
|
+
}
|
|
903
|
+
function legacyStatusMessage(event) {
|
|
904
|
+
const status = event.properties?.status;
|
|
905
|
+
if (!status || typeof status !== "object") return void 0;
|
|
906
|
+
const message = status.message;
|
|
907
|
+
return typeof message === "string" ? message : void 0;
|
|
908
|
+
}
|
|
909
|
+
async function ensureSession({
|
|
910
|
+
client,
|
|
911
|
+
start,
|
|
912
|
+
emit
|
|
913
|
+
}) {
|
|
914
|
+
if (runtime.sessionId) return runtime.sessionId;
|
|
915
|
+
if (start.resumeSessionId) {
|
|
916
|
+
const existing = await legacySessionGet({
|
|
917
|
+
client,
|
|
918
|
+
sessionId: start.resumeSessionId
|
|
919
|
+
}).catch(() => void 0);
|
|
920
|
+
if (existing && !existing.error) {
|
|
921
|
+
runtime.sessionId = start.resumeSessionId;
|
|
922
|
+
emit({ type: "bridge-thread", threadId: runtime.sessionId });
|
|
923
|
+
return runtime.sessionId;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const created = await legacySessionCreate({ client });
|
|
927
|
+
if (created.error) {
|
|
928
|
+
throw new Error(
|
|
929
|
+
`OpenCode session create failed: ${formatError(created.error)}`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
const id = readSessionId(created.data);
|
|
933
|
+
if (!id) throw new Error("OpenCode session create returned no id.");
|
|
934
|
+
runtime.sessionId = id;
|
|
935
|
+
emit({ type: "bridge-thread", threadId: id });
|
|
936
|
+
return id;
|
|
937
|
+
}
|
|
938
|
+
async function runPrompt({
|
|
939
|
+
client,
|
|
940
|
+
sessionId,
|
|
941
|
+
start,
|
|
942
|
+
turn,
|
|
943
|
+
emit
|
|
944
|
+
}) {
|
|
945
|
+
const eventsAbort = new AbortController();
|
|
946
|
+
const turnSettled = createDeferred();
|
|
947
|
+
let sawContent = false;
|
|
948
|
+
let sawFinishStep = false;
|
|
949
|
+
let sawBusy = false;
|
|
950
|
+
let terminalError;
|
|
951
|
+
const initialSessionTokens = await readSessionTokens({
|
|
952
|
+
client,
|
|
953
|
+
sessionId
|
|
954
|
+
}).catch(() => void 0);
|
|
955
|
+
let stepUsage;
|
|
956
|
+
let latestSessionTokens;
|
|
957
|
+
const eventLoop = consumeEvents({
|
|
958
|
+
client,
|
|
959
|
+
sessionId,
|
|
960
|
+
permissionMode: start.permissionMode,
|
|
961
|
+
turn,
|
|
962
|
+
emit: (msg) => {
|
|
963
|
+
if (msg.type === "text-delta" || msg.type === "reasoning-delta") {
|
|
964
|
+
sawContent = true;
|
|
965
|
+
}
|
|
966
|
+
if (msg.type === "finish-step") {
|
|
967
|
+
sawFinishStep = true;
|
|
968
|
+
stepUsage = addUsage({
|
|
969
|
+
left: stepUsage,
|
|
970
|
+
right: msg.usage
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
emit(msg);
|
|
974
|
+
},
|
|
975
|
+
signal: eventsAbort.signal,
|
|
976
|
+
onEvent: (event) => {
|
|
977
|
+
if (event.type === "session.updated") {
|
|
978
|
+
latestSessionTokens = extractSessionTokens(event.properties) ?? latestSessionTokens;
|
|
979
|
+
}
|
|
980
|
+
if (isStepSettlementEvent(event)) {
|
|
981
|
+
turnSettled.resolve();
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
const status = legacyStatusType(event);
|
|
985
|
+
if (status === "busy") {
|
|
986
|
+
sawBusy = true;
|
|
987
|
+
} else if (status === "retry") {
|
|
988
|
+
sawBusy = true;
|
|
989
|
+
terminalError = legacyStatusMessage(event) ?? "Session retry";
|
|
990
|
+
turnSettled.resolve();
|
|
991
|
+
return true;
|
|
992
|
+
} else if (sawBusy && status === "idle") {
|
|
993
|
+
turnSettled.resolve();
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
if (event.type === "session.error") {
|
|
997
|
+
terminalError = formatError(event.properties?.error ?? event);
|
|
998
|
+
turnSettled.resolve();
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}).finally(() => turnSettled.resolve());
|
|
1003
|
+
emit({
|
|
1004
|
+
type: "stream-start",
|
|
1005
|
+
...start.model ? { modelId: start.model } : {}
|
|
1006
|
+
});
|
|
1007
|
+
const prompted = await legacySessionPrompt({
|
|
1008
|
+
client,
|
|
1009
|
+
sessionId,
|
|
1010
|
+
start
|
|
1011
|
+
});
|
|
1012
|
+
if (prompted.error) {
|
|
1013
|
+
eventsAbort.abort();
|
|
1014
|
+
throw new Error(`OpenCode prompt failed: ${formatError(prompted.error)}`);
|
|
1015
|
+
}
|
|
1016
|
+
await turnSettled.promise;
|
|
1017
|
+
eventsAbort.abort();
|
|
1018
|
+
await eventLoop.catch(() => {
|
|
1019
|
+
});
|
|
1020
|
+
if (terminalError) throw new Error(terminalError);
|
|
1021
|
+
if (!sawFinishStep) {
|
|
1022
|
+
const emittedFallback = await emitContextFallback({
|
|
1023
|
+
client,
|
|
1024
|
+
sessionId,
|
|
1025
|
+
emit,
|
|
1026
|
+
emitContent: !sawContent
|
|
1027
|
+
}).catch(() => false);
|
|
1028
|
+
if (!emittedFallback) {
|
|
1029
|
+
emit({
|
|
1030
|
+
type: "finish-step",
|
|
1031
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
1032
|
+
usage: defaultUsage(),
|
|
1033
|
+
harnessMetadata: { opencode: { fallback: true, missingContext: true } }
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const finalSessionTokens = await readSessionTokens({ client, sessionId }).catch(() => void 0) ?? latestSessionTokens;
|
|
1038
|
+
if (initialSessionTokens && finalSessionTokens) {
|
|
1039
|
+
return mapUsage(
|
|
1040
|
+
subtractSessionTokens({
|
|
1041
|
+
before: initialSessionTokens,
|
|
1042
|
+
after: finalSessionTokens
|
|
1043
|
+
})
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return stepUsage;
|
|
1047
|
+
}
|
|
1048
|
+
async function runCompaction({
|
|
1049
|
+
client,
|
|
1050
|
+
sessionId,
|
|
1051
|
+
start,
|
|
1052
|
+
turn,
|
|
1053
|
+
emit
|
|
1054
|
+
}) {
|
|
1055
|
+
const eventsAbort = new AbortController();
|
|
1056
|
+
const compactionSettled = createDeferred();
|
|
1057
|
+
let sawCompaction = false;
|
|
1058
|
+
let sawBusy = false;
|
|
1059
|
+
let terminalError;
|
|
1060
|
+
const model = await resolveCompactionModel({
|
|
1061
|
+
client,
|
|
1062
|
+
sessionId,
|
|
1063
|
+
start
|
|
1064
|
+
});
|
|
1065
|
+
if (!model) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
"OpenCode compaction requires a previous turn or an explicit model."
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const eventLoop = consumeEvents({
|
|
1071
|
+
client,
|
|
1072
|
+
sessionId,
|
|
1073
|
+
permissionMode: start.permissionMode,
|
|
1074
|
+
turn,
|
|
1075
|
+
emit: (msg) => {
|
|
1076
|
+
if (msg.type === "compaction") sawCompaction = true;
|
|
1077
|
+
emit(msg);
|
|
1078
|
+
},
|
|
1079
|
+
signal: eventsAbort.signal,
|
|
1080
|
+
onEvent: (event) => {
|
|
1081
|
+
if (event.type === "session.next.compaction.ended" || event.type === "session.compacted") {
|
|
1082
|
+
compactionSettled.resolve();
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
const status = legacyStatusType(event);
|
|
1086
|
+
if (status === "busy") {
|
|
1087
|
+
sawBusy = true;
|
|
1088
|
+
} else if (status === "retry") {
|
|
1089
|
+
sawBusy = true;
|
|
1090
|
+
terminalError = legacyStatusMessage(event) ?? "Session retry";
|
|
1091
|
+
compactionSettled.resolve();
|
|
1092
|
+
return true;
|
|
1093
|
+
} else if (sawBusy && status === "idle") {
|
|
1094
|
+
compactionSettled.resolve();
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
if (event.type === "session.error") {
|
|
1098
|
+
terminalError = formatError(event.properties?.error ?? event);
|
|
1099
|
+
compactionSettled.resolve();
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
const compacted = await legacySessionSummarize({
|
|
1105
|
+
client,
|
|
1106
|
+
sessionId,
|
|
1107
|
+
model
|
|
1108
|
+
});
|
|
1109
|
+
if (compacted.error) {
|
|
1110
|
+
eventsAbort.abort();
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`OpenCode compaction failed: ${formatError(compacted.error)}`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
await Promise.race([compactionSettled.promise, sleep(250)]);
|
|
1116
|
+
eventsAbort.abort();
|
|
1117
|
+
await eventLoop.catch(() => {
|
|
1118
|
+
});
|
|
1119
|
+
if (terminalError) throw new Error(terminalError);
|
|
1120
|
+
if (!sawCompaction) {
|
|
1121
|
+
emit({
|
|
1122
|
+
type: "compaction",
|
|
1123
|
+
trigger: "manual",
|
|
1124
|
+
summary: "",
|
|
1125
|
+
harnessMetadata: {
|
|
1126
|
+
opencode: { missingSummary: true }
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function consumeEvents({
|
|
1132
|
+
client,
|
|
1133
|
+
sessionId,
|
|
1134
|
+
permissionMode,
|
|
1135
|
+
turn,
|
|
1136
|
+
emit,
|
|
1137
|
+
signal,
|
|
1138
|
+
onEvent
|
|
1139
|
+
}) {
|
|
1140
|
+
const stream = await subscribeLegacyEvents({ client, signal });
|
|
1141
|
+
if (!stream) return;
|
|
1142
|
+
const state = createTranslationState();
|
|
1143
|
+
for await (const rawEvent of stream) {
|
|
1144
|
+
if (signal.aborted || turn.abortSignal.aborted) break;
|
|
1145
|
+
const event = unwrapOpenCodeEvent(rawEvent);
|
|
1146
|
+
const eventSessionId = event ? getOpenCodeEventSessionId(event) : void 0;
|
|
1147
|
+
if (!event || eventSessionId && eventSessionId !== sessionId) continue;
|
|
1148
|
+
await translateAndEmit({
|
|
1149
|
+
event,
|
|
1150
|
+
state,
|
|
1151
|
+
sessionId,
|
|
1152
|
+
permissionMode,
|
|
1153
|
+
client,
|
|
1154
|
+
turn,
|
|
1155
|
+
emit
|
|
1156
|
+
});
|
|
1157
|
+
if (onEvent?.(event)) break;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function createTranslationState() {
|
|
1161
|
+
return {
|
|
1162
|
+
textDeltas: /* @__PURE__ */ new Map(),
|
|
1163
|
+
reasoningDeltas: /* @__PURE__ */ new Map(),
|
|
1164
|
+
toolInputs: /* @__PURE__ */ new Map(),
|
|
1165
|
+
toolNames: /* @__PURE__ */ new Map(),
|
|
1166
|
+
toolCallsEmitted: /* @__PURE__ */ new Set(),
|
|
1167
|
+
toolResultsEmitted: /* @__PURE__ */ new Set(),
|
|
1168
|
+
shellCommands: /* @__PURE__ */ new Map(),
|
|
1169
|
+
messageRoles: /* @__PURE__ */ new Map(),
|
|
1170
|
+
turnUsage: void 0,
|
|
1171
|
+
legacyTextPartIds: /* @__PURE__ */ new Set(),
|
|
1172
|
+
legacyReasoningPartIds: /* @__PURE__ */ new Set()
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
async function translateAndEmit({
|
|
1176
|
+
event,
|
|
1177
|
+
state,
|
|
1178
|
+
sessionId,
|
|
1179
|
+
permissionMode,
|
|
1180
|
+
client,
|
|
1181
|
+
turn,
|
|
1182
|
+
emit
|
|
1183
|
+
}) {
|
|
1184
|
+
const type = event.type;
|
|
1185
|
+
const props = event.properties ?? {};
|
|
1186
|
+
if (type === "message.updated") {
|
|
1187
|
+
const info = props.info;
|
|
1188
|
+
if (isRecord(info)) {
|
|
1189
|
+
const id = stringValue(info.id);
|
|
1190
|
+
const role = stringValue(info.role);
|
|
1191
|
+
if (id && role) state.messageRoles.set(id, role);
|
|
1192
|
+
}
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (type === "message.part.delta") {
|
|
1196
|
+
const field = String(props.field ?? "");
|
|
1197
|
+
const delta = String(props.delta ?? "");
|
|
1198
|
+
if (!delta) return;
|
|
1199
|
+
const messageID = stringValue(props.messageID);
|
|
1200
|
+
if (messageID && state.messageRoles.get(messageID) === "user") return;
|
|
1201
|
+
if (field === "text") {
|
|
1202
|
+
const id = legacyPartId({ value: props, fallback: "legacy-text" });
|
|
1203
|
+
startLegacyPart({ ids: state.legacyTextPartIds, id, emit, type: "text" });
|
|
1204
|
+
state.textDeltas.set(id, `${state.textDeltas.get(id) ?? ""}${delta}`);
|
|
1205
|
+
emit({ type: "text-delta", id, delta });
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (field === "reasoning") {
|
|
1209
|
+
const id = legacyPartId({ value: props, fallback: "legacy-reasoning" });
|
|
1210
|
+
startLegacyPart({
|
|
1211
|
+
ids: state.legacyReasoningPartIds,
|
|
1212
|
+
id,
|
|
1213
|
+
emit,
|
|
1214
|
+
type: "reasoning"
|
|
1215
|
+
});
|
|
1216
|
+
state.reasoningDeltas.set(
|
|
1217
|
+
id,
|
|
1218
|
+
`${state.reasoningDeltas.get(id) ?? ""}${delta}`
|
|
1219
|
+
);
|
|
1220
|
+
emit({ type: "reasoning-delta", id, delta });
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (type === "message.part.updated") {
|
|
1225
|
+
if (emitLegacyTextPartUpdate({ part: props.part, state, emit })) return;
|
|
1226
|
+
emitLegacyToolPart({ part: props.part, state, emit });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (type === "session.next.text.started") {
|
|
1230
|
+
emit({ type: "text-start", id: String(props.textID ?? event.id) });
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (type === "session.next.text.delta") {
|
|
1234
|
+
const id = String(props.textID ?? event.id);
|
|
1235
|
+
state.textDeltas.set(
|
|
1236
|
+
id,
|
|
1237
|
+
`${state.textDeltas.get(id) ?? ""}${String(props.delta ?? "")}`
|
|
1238
|
+
);
|
|
1239
|
+
emit({
|
|
1240
|
+
type: "text-delta",
|
|
1241
|
+
id,
|
|
1242
|
+
delta: String(props.delta ?? "")
|
|
1243
|
+
});
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (type === "session.next.text.ended") {
|
|
1247
|
+
const id = String(props.textID ?? event.id);
|
|
1248
|
+
emitMissingFinalDelta({
|
|
1249
|
+
id,
|
|
1250
|
+
fullText: typeof props.text === "string" ? props.text : void 0,
|
|
1251
|
+
emittedText: state.textDeltas.get(id) ?? "",
|
|
1252
|
+
emit,
|
|
1253
|
+
type: "text-delta"
|
|
1254
|
+
});
|
|
1255
|
+
emit({ type: "text-end", id });
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (type === "session.next.reasoning.started") {
|
|
1259
|
+
emit({
|
|
1260
|
+
type: "reasoning-start",
|
|
1261
|
+
id: String(props.reasoningID ?? event.id)
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (type === "session.next.reasoning.delta") {
|
|
1266
|
+
const id = String(props.reasoningID ?? event.id);
|
|
1267
|
+
state.reasoningDeltas.set(
|
|
1268
|
+
id,
|
|
1269
|
+
`${state.reasoningDeltas.get(id) ?? ""}${String(props.delta ?? "")}`
|
|
1270
|
+
);
|
|
1271
|
+
emit({
|
|
1272
|
+
type: "reasoning-delta",
|
|
1273
|
+
id,
|
|
1274
|
+
delta: String(props.delta ?? "")
|
|
1275
|
+
});
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (type === "session.next.reasoning.ended") {
|
|
1279
|
+
const id = String(props.reasoningID ?? event.id);
|
|
1280
|
+
emitMissingFinalDelta({
|
|
1281
|
+
id,
|
|
1282
|
+
fullText: typeof props.text === "string" ? props.text : void 0,
|
|
1283
|
+
emittedText: state.reasoningDeltas.get(id) ?? "",
|
|
1284
|
+
emit,
|
|
1285
|
+
type: "reasoning-delta"
|
|
1286
|
+
});
|
|
1287
|
+
emit({ type: "reasoning-end", id });
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
if (type === "session.next.shell.started") {
|
|
1291
|
+
const callID = String(props.callID ?? event.id);
|
|
1292
|
+
const command = String(props.command ?? "");
|
|
1293
|
+
state.shellCommands.set(callID, command);
|
|
1294
|
+
emit({
|
|
1295
|
+
type: "tool-call",
|
|
1296
|
+
toolCallId: callID,
|
|
1297
|
+
toolName: "bash",
|
|
1298
|
+
nativeName: "bash",
|
|
1299
|
+
input: JSON.stringify({ command }),
|
|
1300
|
+
providerExecuted: true
|
|
1301
|
+
});
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (type === "session.next.shell.ended") {
|
|
1305
|
+
const callID = String(props.callID ?? event.id);
|
|
1306
|
+
emit({
|
|
1307
|
+
type: "tool-result",
|
|
1308
|
+
toolCallId: callID,
|
|
1309
|
+
toolName: "bash",
|
|
1310
|
+
result: {
|
|
1311
|
+
command: state.shellCommands.get(callID) ?? "",
|
|
1312
|
+
output: String(props.output ?? "")
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (type === "session.next.tool.input.delta") {
|
|
1318
|
+
const callID = String(props.callID ?? event.id);
|
|
1319
|
+
state.toolInputs.set(
|
|
1320
|
+
callID,
|
|
1321
|
+
`${state.toolInputs.get(callID) ?? ""}${String(props.delta ?? "")}`
|
|
1322
|
+
);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (type === "session.next.tool.input.ended") {
|
|
1326
|
+
state.toolInputs.set(
|
|
1327
|
+
String(props.callID ?? event.id),
|
|
1328
|
+
String(props.text ?? "")
|
|
1329
|
+
);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (type === "session.next.tool.called") {
|
|
1333
|
+
const callID = String(props.callID ?? event.id);
|
|
1334
|
+
const rawToolName = String(props.tool ?? "unknown");
|
|
1335
|
+
const toolName = toWireToolName(rawToolName);
|
|
1336
|
+
state.toolNames.set(callID, { rawToolName, toolName });
|
|
1337
|
+
if (isHostTool(toolName, props.tool)) return;
|
|
1338
|
+
emit({
|
|
1339
|
+
type: "tool-call",
|
|
1340
|
+
toolCallId: callID,
|
|
1341
|
+
toolName,
|
|
1342
|
+
...nativeNameField({ nativeName: rawToolName, toolName }),
|
|
1343
|
+
input: JSON.stringify(props.input ?? parseToolInput(state, props)),
|
|
1344
|
+
providerExecuted: true,
|
|
1345
|
+
...props.provider?.metadata ? { providerMetadata: props.provider.metadata } : {}
|
|
1346
|
+
});
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (type === "session.next.tool.success" || type === "session.next.tool.failed") {
|
|
1350
|
+
const callID = String(props.callID ?? event.id);
|
|
1351
|
+
const cachedTool = state.toolNames.get(callID);
|
|
1352
|
+
const rawToolName = cachedTool?.rawToolName ?? String(props.tool ?? "");
|
|
1353
|
+
const toolName = cachedTool?.toolName ?? toWireToolName(rawToolName || "unknown");
|
|
1354
|
+
if (isHostTool(toolName, rawToolName)) return;
|
|
1355
|
+
emit({
|
|
1356
|
+
type: "tool-result",
|
|
1357
|
+
toolCallId: callID,
|
|
1358
|
+
toolName,
|
|
1359
|
+
result: props.result ?? props.structured ?? ("content" in props ? props.content : null) ?? null,
|
|
1360
|
+
...type === "session.next.tool.failed" ? { isError: true } : {}
|
|
1361
|
+
});
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (type === "session.next.step.ended") {
|
|
1365
|
+
closeLegacyOpenParts({ state, emit });
|
|
1366
|
+
state.turnUsage = mapUsage(props.tokens);
|
|
1367
|
+
emit({
|
|
1368
|
+
type: "finish-step",
|
|
1369
|
+
finishReason: {
|
|
1370
|
+
unified: mapFinishReason(String(props.finish ?? "stop")),
|
|
1371
|
+
raw: String(props.finish ?? "stop")
|
|
1372
|
+
},
|
|
1373
|
+
usage: state.turnUsage,
|
|
1374
|
+
...typeof props.cost === "number" ? { harnessMetadata: { opencode: { cost: props.cost } } } : {}
|
|
1375
|
+
});
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (type === "session.next.compaction.ended") {
|
|
1379
|
+
emit({
|
|
1380
|
+
type: "compaction",
|
|
1381
|
+
trigger: props.reason === "auto" ? "auto" : "manual",
|
|
1382
|
+
summary: String(props.text ?? ""),
|
|
1383
|
+
harnessMetadata: {
|
|
1384
|
+
opencode: {
|
|
1385
|
+
recent: String(props.recent ?? "")
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (type === "file.edited") {
|
|
1392
|
+
emit({
|
|
1393
|
+
type: "file-change",
|
|
1394
|
+
event: "modify",
|
|
1395
|
+
path: stripWorkDir(String(props.file ?? ""))
|
|
1396
|
+
});
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (type === "session.error" || type === "session.next.step.failed") {
|
|
1400
|
+
emit({ type: "error", error: formatError(props.error ?? event) });
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (type === "permission.v2.asked") {
|
|
1404
|
+
await handlePermissionV2({
|
|
1405
|
+
client,
|
|
1406
|
+
sessionId,
|
|
1407
|
+
permissionMode,
|
|
1408
|
+
turn,
|
|
1409
|
+
emit,
|
|
1410
|
+
event
|
|
1411
|
+
});
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (type === "permission.asked") {
|
|
1415
|
+
await handlePermission({
|
|
1416
|
+
client,
|
|
1417
|
+
sessionId,
|
|
1418
|
+
permissionMode,
|
|
1419
|
+
turn,
|
|
1420
|
+
emit,
|
|
1421
|
+
event
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
function legacyPartId({
|
|
1426
|
+
value,
|
|
1427
|
+
fallback
|
|
1428
|
+
}) {
|
|
1429
|
+
return stringValue(value.partID) ?? stringValue(value.id) ?? fallback;
|
|
1430
|
+
}
|
|
1431
|
+
function startLegacyPart({
|
|
1432
|
+
ids,
|
|
1433
|
+
id,
|
|
1434
|
+
emit,
|
|
1435
|
+
type
|
|
1436
|
+
}) {
|
|
1437
|
+
if (ids.has(id)) return;
|
|
1438
|
+
ids.add(id);
|
|
1439
|
+
emit({ type: `${type}-start`, id });
|
|
1440
|
+
}
|
|
1441
|
+
function emitLegacyTextPartUpdate({
|
|
1442
|
+
part,
|
|
1443
|
+
state,
|
|
1444
|
+
emit
|
|
1445
|
+
}) {
|
|
1446
|
+
if (!isRecord(part)) return false;
|
|
1447
|
+
if (part.type !== "text" && part.type !== "reasoning") return false;
|
|
1448
|
+
const id = stringValue(part.id);
|
|
1449
|
+
if (!id) return true;
|
|
1450
|
+
const messageID = stringValue(part.messageID);
|
|
1451
|
+
if (messageID && state.messageRoles.get(messageID) === "user") return true;
|
|
1452
|
+
const isReasoning = part.type === "reasoning";
|
|
1453
|
+
const ids = isReasoning ? state.legacyReasoningPartIds : state.legacyTextPartIds;
|
|
1454
|
+
const deltaMap = isReasoning ? state.reasoningDeltas : state.textDeltas;
|
|
1455
|
+
const deltaType = isReasoning ? "reasoning-delta" : "text-delta";
|
|
1456
|
+
const text = typeof part.text === "string" ? part.text : void 0;
|
|
1457
|
+
startLegacyPart({
|
|
1458
|
+
ids,
|
|
1459
|
+
id,
|
|
1460
|
+
emit,
|
|
1461
|
+
type: isReasoning ? "reasoning" : "text"
|
|
1462
|
+
});
|
|
1463
|
+
if (text !== void 0) {
|
|
1464
|
+
emitMissingFinalDelta({
|
|
1465
|
+
id,
|
|
1466
|
+
fullText: text,
|
|
1467
|
+
emittedText: deltaMap.get(id) ?? "",
|
|
1468
|
+
emit,
|
|
1469
|
+
type: deltaType
|
|
1470
|
+
});
|
|
1471
|
+
deltaMap.set(id, text);
|
|
1472
|
+
}
|
|
1473
|
+
if (legacyPartEnded(part)) {
|
|
1474
|
+
ids.delete(id);
|
|
1475
|
+
deltaMap.delete(id);
|
|
1476
|
+
emit({ type: isReasoning ? "reasoning-end" : "text-end", id });
|
|
1477
|
+
}
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
function legacyPartEnded(part) {
|
|
1481
|
+
return isRecord(part.time) && part.time.end != null;
|
|
1482
|
+
}
|
|
1483
|
+
function closeLegacyOpenParts({
|
|
1484
|
+
state,
|
|
1485
|
+
emit
|
|
1486
|
+
}) {
|
|
1487
|
+
for (const id of state.legacyReasoningPartIds) {
|
|
1488
|
+
emit({ type: "reasoning-end", id });
|
|
1489
|
+
state.reasoningDeltas.delete(id);
|
|
1490
|
+
}
|
|
1491
|
+
state.legacyReasoningPartIds.clear();
|
|
1492
|
+
for (const id of state.legacyTextPartIds) {
|
|
1493
|
+
emit({ type: "text-end", id });
|
|
1494
|
+
state.textDeltas.delete(id);
|
|
1495
|
+
}
|
|
1496
|
+
state.legacyTextPartIds.clear();
|
|
1497
|
+
}
|
|
1498
|
+
function emitLegacyToolPart({
|
|
1499
|
+
part,
|
|
1500
|
+
state,
|
|
1501
|
+
emit
|
|
1502
|
+
}) {
|
|
1503
|
+
if (!part || typeof part !== "object") return;
|
|
1504
|
+
const toolPart = part;
|
|
1505
|
+
if (toolPart.type !== "tool") return;
|
|
1506
|
+
const status = legacyToolPartStatus(toolPart);
|
|
1507
|
+
if (status !== "running" && status !== "completed" && status !== "error") {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
if (typeof toolPart.tool !== "string" || typeof toolPart.callID !== "string") {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const callID = toolPart.callID;
|
|
1514
|
+
const rawToolName = toolPart.tool;
|
|
1515
|
+
const toolName = toWireToolName(rawToolName);
|
|
1516
|
+
state.toolNames.set(callID, { rawToolName, toolName });
|
|
1517
|
+
if (isHostTool(toolName, rawToolName)) return;
|
|
1518
|
+
if (!state.toolCallsEmitted.has(callID)) {
|
|
1519
|
+
state.toolCallsEmitted.add(callID);
|
|
1520
|
+
emit({
|
|
1521
|
+
type: "tool-call",
|
|
1522
|
+
toolCallId: callID,
|
|
1523
|
+
toolName,
|
|
1524
|
+
...nativeNameField({ nativeName: rawToolName, toolName }),
|
|
1525
|
+
input: JSON.stringify(legacyToolPartInput(toolPart)),
|
|
1526
|
+
providerExecuted: true,
|
|
1527
|
+
...toolPart.provider?.metadata ? { providerMetadata: toolPart.provider.metadata } : {}
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
if ((status === "completed" || status === "error") && !state.toolResultsEmitted.has(callID)) {
|
|
1531
|
+
state.toolResultsEmitted.add(callID);
|
|
1532
|
+
emit({
|
|
1533
|
+
type: "tool-result",
|
|
1534
|
+
toolCallId: callID,
|
|
1535
|
+
toolName,
|
|
1536
|
+
result: legacyToolPartOutput(toolPart),
|
|
1537
|
+
...status === "error" ? { isError: true } : {}
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function legacyToolPartStatus(part) {
|
|
1542
|
+
return typeof part.state === "string" ? part.state : typeof part.state === "object" && part.state !== null ? String(part.state.status ?? "") : void 0;
|
|
1543
|
+
}
|
|
1544
|
+
function legacyToolPartInput(part) {
|
|
1545
|
+
const state = typeof part.state === "object" && part.state !== null ? part.state : void 0;
|
|
1546
|
+
return {
|
|
1547
|
+
...isRecord(part.metadata) ? part.metadata : {},
|
|
1548
|
+
...isRecord(state?.metadata) ? state.metadata : {},
|
|
1549
|
+
...isRecord(state?.input) ? state.input : {}
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function legacyToolPartOutput(part) {
|
|
1553
|
+
const state = typeof part.state === "object" && part.state !== null ? part.state : void 0;
|
|
1554
|
+
if (state?.status === "error") {
|
|
1555
|
+
return state.error ?? part.error ?? state.result ?? "tool failed";
|
|
1556
|
+
}
|
|
1557
|
+
return state?.output ?? state?.result ?? state?.structured ?? state?.content ?? null;
|
|
1558
|
+
}
|
|
1559
|
+
function isRecord(value) {
|
|
1560
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1561
|
+
}
|
|
1562
|
+
function parseToolInput(state, props) {
|
|
1563
|
+
const text = state.toolInputs.get(String(props.callID ?? ""));
|
|
1564
|
+
if (!text) return {};
|
|
1565
|
+
try {
|
|
1566
|
+
return JSON.parse(text);
|
|
1567
|
+
} catch {
|
|
1568
|
+
return { input: text };
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
async function handlePermissionV2({
|
|
1572
|
+
client,
|
|
1573
|
+
sessionId,
|
|
1574
|
+
permissionMode,
|
|
1575
|
+
turn,
|
|
1576
|
+
emit,
|
|
1577
|
+
event
|
|
1578
|
+
}) {
|
|
1579
|
+
const props = event.properties ?? {};
|
|
1580
|
+
const requestID = String(props.id ?? "");
|
|
1581
|
+
if (!requestID) return;
|
|
1582
|
+
const reply = await selectPermissionReply({
|
|
1583
|
+
action: String(props.action ?? ""),
|
|
1584
|
+
resources: Array.isArray(props.resources) ? props.resources.map(String) : [],
|
|
1585
|
+
requestID,
|
|
1586
|
+
toolCallId: typeof props.source === "object" && props.source !== null && "callID" in props.source ? String(props.source.callID) : requestID,
|
|
1587
|
+
permissionMode,
|
|
1588
|
+
turn,
|
|
1589
|
+
emit
|
|
1590
|
+
});
|
|
1591
|
+
await client.v2.session.permission.reply({
|
|
1592
|
+
sessionID: sessionId,
|
|
1593
|
+
requestID,
|
|
1594
|
+
reply: reply.reply,
|
|
1595
|
+
...reply.message ? { message: reply.message } : {}
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
async function handlePermission({
|
|
1599
|
+
client,
|
|
1600
|
+
sessionId,
|
|
1601
|
+
permissionMode,
|
|
1602
|
+
turn,
|
|
1603
|
+
emit,
|
|
1604
|
+
event
|
|
1605
|
+
}) {
|
|
1606
|
+
const props = event.properties ?? {};
|
|
1607
|
+
const requestID = String(props.id ?? "");
|
|
1608
|
+
if (!requestID) return;
|
|
1609
|
+
const reply = await selectPermissionReply({
|
|
1610
|
+
action: String(props.permission ?? ""),
|
|
1611
|
+
resources: Array.isArray(props.patterns) ? props.patterns.map(String) : [],
|
|
1612
|
+
requestID,
|
|
1613
|
+
toolCallId: typeof props.tool === "object" && props.tool !== null && "callID" in props.tool ? String(props.tool.callID) : requestID,
|
|
1614
|
+
permissionMode,
|
|
1615
|
+
turn,
|
|
1616
|
+
emit
|
|
1617
|
+
});
|
|
1618
|
+
await client.permission.reply({
|
|
1619
|
+
requestID,
|
|
1620
|
+
directory: workdir,
|
|
1621
|
+
reply: reply.reply,
|
|
1622
|
+
...reply.message ? { message: reply.message } : {}
|
|
1623
|
+
});
|
|
1624
|
+
void sessionId;
|
|
1625
|
+
}
|
|
1626
|
+
async function selectPermissionReply({
|
|
1627
|
+
action,
|
|
1628
|
+
resources,
|
|
1629
|
+
requestID,
|
|
1630
|
+
toolCallId,
|
|
1631
|
+
permissionMode,
|
|
1632
|
+
turn,
|
|
1633
|
+
emit
|
|
1634
|
+
}) {
|
|
1635
|
+
const toolName = toPermissionToolName(action);
|
|
1636
|
+
if (resources.some((resource) => isExternalPath(resource))) {
|
|
1637
|
+
return { reply: "reject", message: "External directory access rejected." };
|
|
1638
|
+
}
|
|
1639
|
+
if (!permissionMode || permissionMode === "allow-all") {
|
|
1640
|
+
return { reply: "always" };
|
|
1641
|
+
}
|
|
1642
|
+
const kind = TOOL_KIND[toolName] ?? "bash";
|
|
1643
|
+
const allowed = permissionMode === "allow-edits" ? kind === "readonly" || kind === "edit" : kind === "readonly";
|
|
1644
|
+
if (allowed) return { reply: "always" };
|
|
1645
|
+
emit({
|
|
1646
|
+
type: "tool-approval-request",
|
|
1647
|
+
approvalId: requestID,
|
|
1648
|
+
toolCallId
|
|
1649
|
+
});
|
|
1650
|
+
const decision = await turn.requestToolApproval(requestID);
|
|
1651
|
+
return decision.approved ? { reply: "once" } : {
|
|
1652
|
+
reply: "reject",
|
|
1653
|
+
...decision.reason ? { message: decision.reason } : {}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
function toPermissionToolName(action) {
|
|
1657
|
+
const normalized = action.toLowerCase();
|
|
1658
|
+
if (normalized.includes("bash") || normalized.includes("shell"))
|
|
1659
|
+
return "bash";
|
|
1660
|
+
if (normalized.includes("edit")) return "edit";
|
|
1661
|
+
if (normalized.includes("write")) return "write";
|
|
1662
|
+
if (normalized.includes("webfetch")) return "webfetch";
|
|
1663
|
+
if (normalized.includes("task") || normalized.includes("agent"))
|
|
1664
|
+
return "agent";
|
|
1665
|
+
if (normalized.includes("list")) return "ls";
|
|
1666
|
+
if (normalized.includes("grep")) return "grep";
|
|
1667
|
+
if (normalized.includes("glob")) return "glob";
|
|
1668
|
+
if (normalized.includes("read")) return "read";
|
|
1669
|
+
return toWireToolName(normalized);
|
|
1670
|
+
}
|
|
1671
|
+
function isExternalPath(resource) {
|
|
1672
|
+
if (!path2.isAbsolute(resource)) return false;
|
|
1673
|
+
const normalized = path2.resolve(resource);
|
|
1674
|
+
return !isPathInsideOrEqual(normalized, workdir) && (!skillsDir || !isPathInsideOrEqual(normalized, skillsDir));
|
|
1675
|
+
}
|
|
1676
|
+
function isPathInsideOrEqual(file, root) {
|
|
1677
|
+
const normalizedRoot = path2.resolve(root);
|
|
1678
|
+
return file === normalizedRoot || file.startsWith(`${normalizedRoot}/`);
|
|
1679
|
+
}
|
|
1680
|
+
function toWireToolName(nativeName) {
|
|
1681
|
+
return NATIVE_TO_COMMON[nativeName] ?? OPENCODE_TO_WIRE[nativeName] ?? nativeName;
|
|
1682
|
+
}
|
|
1683
|
+
function nativeNameField({
|
|
1684
|
+
nativeName,
|
|
1685
|
+
toolName
|
|
1686
|
+
}) {
|
|
1687
|
+
if (!nativeName || nativeName === toolName || toolName === "agent") return {};
|
|
1688
|
+
return { nativeName };
|
|
1689
|
+
}
|
|
1690
|
+
function isHostTool(toolName, rawToolName) {
|
|
1691
|
+
if (runtime.toolNames.has(toolName)) return true;
|
|
1692
|
+
if (typeof rawToolName === "string" && runtime.toolNames.has(rawToolName)) {
|
|
1693
|
+
return true;
|
|
1694
|
+
}
|
|
1695
|
+
if (typeof rawToolName === "string" && rawToolName.startsWith("harness-tools_") && runtime.toolNames.has(rawToolName.slice("harness-tools_".length))) {
|
|
1696
|
+
return true;
|
|
1697
|
+
}
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
async function emitContextFallback({
|
|
1701
|
+
client,
|
|
1702
|
+
sessionId,
|
|
1703
|
+
emit,
|
|
1704
|
+
emitContent
|
|
1705
|
+
}) {
|
|
1706
|
+
const assistant = await latestAssistantSnapshot({ client, sessionId });
|
|
1707
|
+
if (!assistant) return false;
|
|
1708
|
+
if (emitContent && Array.isArray(assistant.contentParts)) {
|
|
1709
|
+
for (const part of assistant.contentParts) {
|
|
1710
|
+
emitAssistantContentPart(part, emit);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const rawFinish = typeof assistant.finish === "string" ? assistant.finish : assistant.error ? "error" : "stop";
|
|
1714
|
+
emit({
|
|
1715
|
+
type: "finish-step",
|
|
1716
|
+
finishReason: {
|
|
1717
|
+
unified: mapFinishReason(rawFinish),
|
|
1718
|
+
raw: rawFinish
|
|
1719
|
+
},
|
|
1720
|
+
usage: mapUsage(assistant.tokens),
|
|
1721
|
+
...typeof assistant.cost === "number" ? {
|
|
1722
|
+
harnessMetadata: {
|
|
1723
|
+
opencode: { cost: assistant.cost, fallback: true }
|
|
1724
|
+
}
|
|
1725
|
+
} : { harnessMetadata: { opencode: { fallback: true } } }
|
|
1726
|
+
});
|
|
1727
|
+
return true;
|
|
1728
|
+
}
|
|
1729
|
+
async function readSessionTokens({
|
|
1730
|
+
client,
|
|
1731
|
+
sessionId
|
|
1732
|
+
}) {
|
|
1733
|
+
const session = await legacySessionGet({ client, sessionId });
|
|
1734
|
+
if (session.error) return void 0;
|
|
1735
|
+
return extractSessionTokens(session.data);
|
|
1736
|
+
}
|
|
1737
|
+
async function latestAssistantSnapshot({
|
|
1738
|
+
client,
|
|
1739
|
+
sessionId
|
|
1740
|
+
}) {
|
|
1741
|
+
const legacy = await client.session.messages({ sessionID: sessionId, limit: 20 }).catch(() => void 0);
|
|
1742
|
+
const legacyAssistant = latestLegacyAssistantMessage(legacy?.data);
|
|
1743
|
+
if (legacyAssistant) return legacyAssistant;
|
|
1744
|
+
const context = await client.v2.session.context({ sessionID: sessionId }).catch(() => void 0);
|
|
1745
|
+
if (!context || context.error) return void 0;
|
|
1746
|
+
return latestV2AssistantMessage(context.data);
|
|
1747
|
+
}
|
|
1748
|
+
function latestLegacyAssistantMessage(data) {
|
|
1749
|
+
const messages = Array.isArray(data) ? data : [];
|
|
1750
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1751
|
+
const item = messages[i];
|
|
1752
|
+
if (!item || typeof item !== "object") continue;
|
|
1753
|
+
const record = item;
|
|
1754
|
+
const info = record.info;
|
|
1755
|
+
if (info && typeof info === "object" && info.role === "assistant") {
|
|
1756
|
+
return {
|
|
1757
|
+
...info,
|
|
1758
|
+
contentParts: Array.isArray(record.parts) ? record.parts : void 0
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return void 0;
|
|
1763
|
+
}
|
|
1764
|
+
function latestV2AssistantMessage(data) {
|
|
1765
|
+
const messages = data && typeof data === "object" && Array.isArray(data.data) ? data.data : Array.isArray(data) ? data : [];
|
|
1766
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1767
|
+
const message = messages[i];
|
|
1768
|
+
if (message && typeof message === "object" && message.type === "assistant") {
|
|
1769
|
+
const record = message;
|
|
1770
|
+
return {
|
|
1771
|
+
...record,
|
|
1772
|
+
contentParts: Array.isArray(record.content) ? record.content : void 0
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return void 0;
|
|
1777
|
+
}
|
|
1778
|
+
function emitAssistantContentPart(part, emit) {
|
|
1779
|
+
if (!part || typeof part !== "object") return;
|
|
1780
|
+
const value = part;
|
|
1781
|
+
if (value.type !== "text" && value.type !== "reasoning") return;
|
|
1782
|
+
const id = typeof value.id === "string" && value.id.length > 0 ? value.id : `${value.type}-${randomUUID()}`;
|
|
1783
|
+
const text = typeof value.text === "string" ? value.text : "";
|
|
1784
|
+
if (value.type === "text") {
|
|
1785
|
+
emit({ type: "text-start", id });
|
|
1786
|
+
if (text) emit({ type: "text-delta", id, delta: text });
|
|
1787
|
+
emit({ type: "text-end", id });
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
emit({ type: "reasoning-start", id });
|
|
1791
|
+
if (text) emit({ type: "reasoning-delta", id, delta: text });
|
|
1792
|
+
emit({ type: "reasoning-end", id });
|
|
1793
|
+
}
|
|
1794
|
+
function mapFinishReason(reason) {
|
|
1795
|
+
const normalized = reason.toLowerCase();
|
|
1796
|
+
if (normalized.includes("length")) return "length";
|
|
1797
|
+
if (normalized.includes("filter")) return "content-filter";
|
|
1798
|
+
if (normalized.includes("tool")) return "tool-calls";
|
|
1799
|
+
if (normalized.includes("error") || normalized.includes("fail"))
|
|
1800
|
+
return "error";
|
|
1801
|
+
if (normalized === "stop" || normalized === "end") return "stop";
|
|
1802
|
+
return "other";
|
|
1803
|
+
}
|
|
1804
|
+
async function startToolRelay({
|
|
1805
|
+
relayToken,
|
|
1806
|
+
tools,
|
|
1807
|
+
emit,
|
|
1808
|
+
requestToolResult
|
|
1809
|
+
}) {
|
|
1810
|
+
const toolNames = new Set(tools.map((t) => t.name));
|
|
1811
|
+
const server = createServer(async (req, res) => {
|
|
1812
|
+
try {
|
|
1813
|
+
if (req.method !== "POST" || req.url !== "/" || req.headers.authorization !== `Bearer ${relayToken}`) {
|
|
1814
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1815
|
+
res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const chunks = [];
|
|
1819
|
+
for await (const chunk of req) {
|
|
1820
|
+
chunks.push(chunk);
|
|
1821
|
+
}
|
|
1822
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
1823
|
+
const { requestId, toolName, input } = JSON.parse(body);
|
|
1824
|
+
if (!toolNames.has(toolName)) {
|
|
1825
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1826
|
+
res.end(
|
|
1827
|
+
JSON.stringify({ error: `Tool "${toolName}" is not available` })
|
|
1828
|
+
);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
emit({
|
|
1832
|
+
type: "tool-call",
|
|
1833
|
+
toolCallId: requestId,
|
|
1834
|
+
toolName,
|
|
1835
|
+
input: JSON.stringify(input ?? {}),
|
|
1836
|
+
providerExecuted: false
|
|
1837
|
+
});
|
|
1838
|
+
const { output, isError } = await requestToolResult(requestId);
|
|
1839
|
+
emit({
|
|
1840
|
+
type: "tool-result",
|
|
1841
|
+
toolCallId: requestId,
|
|
1842
|
+
toolName,
|
|
1843
|
+
result: output ?? null,
|
|
1844
|
+
isError: !!isError
|
|
1845
|
+
});
|
|
1846
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1847
|
+
res.end(JSON.stringify({ result: output }));
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1850
|
+
res.end(
|
|
1851
|
+
JSON.stringify({
|
|
1852
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1853
|
+
})
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
await new Promise(
|
|
1858
|
+
(resolve) => server.listen(0, "127.0.0.1", () => resolve())
|
|
1859
|
+
);
|
|
1860
|
+
const address = server.address();
|
|
1861
|
+
if (!address || typeof address === "string") {
|
|
1862
|
+
throw new Error("tool relay did not expose a numeric port");
|
|
1863
|
+
}
|
|
1864
|
+
return {
|
|
1865
|
+
port: address.port,
|
|
1866
|
+
close: () => closeServer(server)
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
function closeServer(server) {
|
|
1870
|
+
try {
|
|
1871
|
+
server.close();
|
|
1872
|
+
} catch {
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
function createDeferred() {
|
|
1876
|
+
let resolve;
|
|
1877
|
+
const promise = new Promise((res) => {
|
|
1878
|
+
resolve = res;
|
|
1879
|
+
});
|
|
1880
|
+
return { promise, resolve };
|
|
1881
|
+
}
|
|
1882
|
+
function sleep(ms) {
|
|
1883
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1884
|
+
}
|
|
1885
|
+
function splitModel(model, provider) {
|
|
1886
|
+
if (!model) return {};
|
|
1887
|
+
if (model.includes("/")) {
|
|
1888
|
+
const [providerID, ...rest] = model.split("/");
|
|
1889
|
+
return { providerID, modelID: rest.join("/") };
|
|
1890
|
+
}
|
|
1891
|
+
return { providerID: provider, modelID: model };
|
|
1892
|
+
}
|
|
1893
|
+
async function resolveCompactionModel({
|
|
1894
|
+
client,
|
|
1895
|
+
sessionId,
|
|
1896
|
+
start
|
|
1897
|
+
}) {
|
|
1898
|
+
const assistant = await latestAssistantSnapshot({ client, sessionId }).catch(
|
|
1899
|
+
() => void 0
|
|
1900
|
+
);
|
|
1901
|
+
const assistantModel = modelRefFromAssistantSnapshot(assistant);
|
|
1902
|
+
if (assistantModel) return assistantModel;
|
|
1903
|
+
const session = await legacySessionGet({ client, sessionId }).catch(
|
|
1904
|
+
() => void 0
|
|
1905
|
+
);
|
|
1906
|
+
const sessionModel = modelRefFromSessionInfo(session?.data);
|
|
1907
|
+
if (sessionModel) return sessionModel;
|
|
1908
|
+
return modelRefFromStart(start);
|
|
1909
|
+
}
|
|
1910
|
+
function modelRefFromAssistantSnapshot(assistant) {
|
|
1911
|
+
if (!assistant) return void 0;
|
|
1912
|
+
const model = modelRefFromValue(assistant.model);
|
|
1913
|
+
if (model) return model;
|
|
1914
|
+
const direct = modelRefFromValue(assistant);
|
|
1915
|
+
if (direct) return direct;
|
|
1916
|
+
if (isRecord(assistant.metadata)) {
|
|
1917
|
+
return modelRefFromValue(assistant.metadata.assistant);
|
|
1918
|
+
}
|
|
1919
|
+
return void 0;
|
|
1920
|
+
}
|
|
1921
|
+
function modelRefFromSessionInfo(data) {
|
|
1922
|
+
if (!isRecord(data)) return void 0;
|
|
1923
|
+
return modelRefFromValue(data.model) ?? modelRefFromValue(data);
|
|
1924
|
+
}
|
|
1925
|
+
function modelRefFromStart(start) {
|
|
1926
|
+
const model = splitModel(start.model, start.provider);
|
|
1927
|
+
if (!model.modelID) return void 0;
|
|
1928
|
+
return {
|
|
1929
|
+
providerID: model.providerID ?? start.provider ?? procEnv2.OPENAI_NAME ?? "anthropic",
|
|
1930
|
+
modelID: model.modelID
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
function modelRefFromValue(value) {
|
|
1934
|
+
if (!isRecord(value)) return void 0;
|
|
1935
|
+
const providerID = stringValue(value.providerID);
|
|
1936
|
+
const modelID = stringValue(value.modelID ?? value.id);
|
|
1937
|
+
if (!providerID || !modelID) return void 0;
|
|
1938
|
+
return { providerID, modelID };
|
|
1939
|
+
}
|
|
1940
|
+
function stringValue(value) {
|
|
1941
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1942
|
+
}
|
|
1943
|
+
function stripWorkDir(file) {
|
|
1944
|
+
if (!file) return file;
|
|
1945
|
+
const normalized = path2.resolve(file);
|
|
1946
|
+
const root = path2.resolve(workdir);
|
|
1947
|
+
return normalized.startsWith(`${root}/`) ? normalized.slice(root.length + 1) : file;
|
|
1948
|
+
}
|
|
1949
|
+
function parseArgs(args2) {
|
|
1950
|
+
const out = {};
|
|
1951
|
+
for (let i = 0; i < args2.length; i++) {
|
|
1952
|
+
if (args2[i] === "--workdir" && i + 1 < args2.length) {
|
|
1953
|
+
out.workdir = args2[++i];
|
|
1954
|
+
} else if (args2[i] === "--bridge-state-dir" && i + 1 < args2.length) {
|
|
1955
|
+
out.bridgeStateDir = args2[++i];
|
|
1956
|
+
} else if (args2[i] === "--bootstrap-dir" && i + 1 < args2.length) {
|
|
1957
|
+
out.bootstrapDir = args2[++i];
|
|
1958
|
+
} else if (args2[i] === "--skills-dir" && i + 1 < args2.length) {
|
|
1959
|
+
out.skillsDir = args2[++i];
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return out;
|
|
1963
|
+
}
|
|
1964
|
+
function formatError(error) {
|
|
1965
|
+
if (error instanceof Error) return error.message;
|
|
1966
|
+
if (typeof error === "string") return error;
|
|
1967
|
+
try {
|
|
1968
|
+
return JSON.stringify(error);
|
|
1969
|
+
} catch {
|
|
1970
|
+
return String(error);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
function serialiseError2(err) {
|
|
1974
|
+
if (err instanceof Error) {
|
|
1975
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
1976
|
+
}
|
|
1977
|
+
return err;
|
|
1978
|
+
}
|
|
1979
|
+
function emitFatal(message) {
|
|
1980
|
+
process.stderr.write(`[opencode bridge] ${message}
|
|
1981
|
+
`);
|
|
1982
|
+
process.exit(1);
|
|
1983
|
+
}
|
|
1984
|
+
//# sourceMappingURL=index.mjs.map
|