@ai-sdk/harness-acp 0.0.0 → 1.0.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 +13 -0
- package/LICENSE +13 -0
- package/README.md +70 -3
- package/dist/bridge/host-tool-mcp.mjs +302 -0
- package/dist/bridge/host-tool-mcp.mjs.map +1 -0
- package/dist/bridge/index.mjs +3278 -0
- package/dist/bridge/index.mjs.map +1 -0
- package/dist/bridge/package.json +12 -0
- package/dist/bridge/pnpm-lock.yaml +816 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +2094 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -10
- package/src/acp-auth.ts +254 -0
- package/src/acp-bridge-protocol.ts +13 -0
- package/src/acp-harness.ts +160 -0
- package/src/index.ts +19 -0
- package/src/v1/acp-v1-bridge-protocol.ts +151 -0
- package/src/v1/acp-v1-harness.ts +1447 -0
- package/src/v1/acp-v1-lifecycle.ts +115 -0
- package/src/v1/acp-v1-prompt.ts +122 -0
- package/src/v1/acp-v1-settings.ts +98 -0
- package/src/v1/acp-v1-skills.ts +228 -0
- package/src/v1/acp-v1-turn-start-config.ts +93 -0
- package/src/v1/bridge/acp-diagnostics.ts +57 -0
- package/src/v1/bridge/acp-stream-capture.ts +152 -0
- package/src/v1/bridge/acp-v1-bridge-environment.ts +95 -0
- package/src/v1/bridge/create-emit-stream-event.ts +97 -0
- package/src/v1/bridge/host-tool-correlation.ts +640 -0
- package/src/v1/bridge/host-tool-mcp-server.ts +180 -0
- package/src/v1/bridge/host-tool-mcp.ts +185 -0
- package/src/v1/bridge/host-tool-relay-client.ts +53 -0
- package/src/v1/bridge/host-tool-relay.ts +510 -0
- package/src/v1/bridge/index.ts +729 -0
- package/src/v1/bridge/package.json +12 -0
- package/src/v1/bridge/permission-controller.ts +152 -0
- package/src/v1/bridge/permission-mode.ts +163 -0
- package/src/v1/bridge/pnpm-lock.yaml +816 -0
- package/src/v1/bridge/profile-values.ts +97 -0
- package/src/v1/bridge/protocol-configuration.ts +146 -0
- package/src/v1/bridge/recovered-session.ts +145 -0
- package/src/v1/bridge/refresh-host-tool-catalog.ts +33 -0
- package/src/v1/bridge/session-lifecycle.ts +83 -0
- package/src/v1/bridge/stream-translator.ts +901 -0
- package/src/v1/implementation.ts +282 -0
- package/src/v1/index.ts +16 -0
- package/src/version.ts +6 -0
- package/index.js +0 -1
|
@@ -0,0 +1,3278 @@
|
|
|
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 name = "AI_HarnessBridgeCapabilityUnsupportedError";
|
|
7
|
+
var HarnessBridgeCapabilityUnsupportedError = class extends Error {
|
|
8
|
+
constructor({
|
|
9
|
+
message,
|
|
10
|
+
harnessId,
|
|
11
|
+
cause
|
|
12
|
+
}) {
|
|
13
|
+
super(message);
|
|
14
|
+
Object.defineProperty(this, "name", { value: name });
|
|
15
|
+
this.harnessId = harnessId;
|
|
16
|
+
this.cause = cause;
|
|
17
|
+
}
|
|
18
|
+
static isInstance(error) {
|
|
19
|
+
return error != null && typeof error === "object" && "name" in error && error.name === name && "message" in error && typeof error.message === "string";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var DEBUG_LEVEL_WEIGHT = {
|
|
23
|
+
error: 0,
|
|
24
|
+
warn: 1,
|
|
25
|
+
info: 2,
|
|
26
|
+
debug: 3,
|
|
27
|
+
trace: 4
|
|
28
|
+
};
|
|
29
|
+
function subsystemMatches(filters, subsystem) {
|
|
30
|
+
if (!filters || filters.length === 0) return true;
|
|
31
|
+
return filters.some(
|
|
32
|
+
(filter) => subsystem === filter || subsystem.startsWith(`${filter}.`)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
function formatBridgeError(err) {
|
|
36
|
+
if (err instanceof Error) {
|
|
37
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
38
|
+
}
|
|
39
|
+
if (typeof err === "string") {
|
|
40
|
+
return { message: err };
|
|
41
|
+
}
|
|
42
|
+
if (err !== null && typeof err === "object") {
|
|
43
|
+
try {
|
|
44
|
+
return { message: JSON.stringify(err) };
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { message: String(err) };
|
|
49
|
+
}
|
|
50
|
+
function parseEnvList(value) {
|
|
51
|
+
if (!value) return void 0;
|
|
52
|
+
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
53
|
+
return items.length > 0 ? items : void 0;
|
|
54
|
+
}
|
|
55
|
+
var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
56
|
+
var WS_OPEN = 1;
|
|
57
|
+
async function runBridge(options) {
|
|
58
|
+
const { bridgeType: bridgeType2, bridgeStateDir: bridgeStateDir2, onStart, onStop, onDestroy } = options;
|
|
59
|
+
const expectedToken = options.token ?? procEnv.BRIDGE_CHANNEL_TOKEN ?? "";
|
|
60
|
+
const bridgeWsPort = options.port ?? parseInt(procEnv.BRIDGE_WS_PORT ?? "0", 10);
|
|
61
|
+
const bridgeMetaPath = `${bridgeStateDir2}/bridge-meta.json`;
|
|
62
|
+
const startConfigPath = `${bridgeStateDir2}/start-config.json`;
|
|
63
|
+
const rerunStartConfigPath = `${bridgeStateDir2}/rerun-start-config.json`;
|
|
64
|
+
const eventLogPath = `${bridgeStateDir2}/event-log.ndjson`;
|
|
65
|
+
try {
|
|
66
|
+
await mkdir(bridgeStateDir2, { recursive: true });
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
let currentBoundPort = 0;
|
|
70
|
+
let currentTurnState = "init";
|
|
71
|
+
let activeSocket;
|
|
72
|
+
let activeSocketReadyForLiveEvents = false;
|
|
73
|
+
let isFirstTurn = true;
|
|
74
|
+
let turnAbort;
|
|
75
|
+
let currentUserMessages;
|
|
76
|
+
let debugConfig;
|
|
77
|
+
let consoleCaptureInstalled = false;
|
|
78
|
+
const envDebugEnabled = ENV_TRUTHY.has(
|
|
79
|
+
(procEnv.HARNESS_DEBUG ?? "").toLowerCase()
|
|
80
|
+
);
|
|
81
|
+
let seqCounter = 0;
|
|
82
|
+
let eventLog = [];
|
|
83
|
+
let diskBuffer = "";
|
|
84
|
+
let flushPromise = null;
|
|
85
|
+
const flushEventsToDisk = async () => {
|
|
86
|
+
while (diskBuffer.length > 0) {
|
|
87
|
+
const buf = diskBuffer;
|
|
88
|
+
diskBuffer = "";
|
|
89
|
+
await appendFile(eventLogPath, buf).catch(() => {
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const scheduleEventFlush = () => {
|
|
94
|
+
if (flushPromise) return;
|
|
95
|
+
flushPromise = new Promise((resolve) => {
|
|
96
|
+
setImmediate(() => {
|
|
97
|
+
void flushEventsToDisk().finally(resolve);
|
|
98
|
+
});
|
|
99
|
+
}).finally(() => {
|
|
100
|
+
flushPromise = null;
|
|
101
|
+
if (diskBuffer.length > 0) {
|
|
102
|
+
scheduleEventFlush();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const flushPendingEventsToDisk = async () => {
|
|
107
|
+
if (diskBuffer.length > 0 && !flushPromise) {
|
|
108
|
+
scheduleEventFlush();
|
|
109
|
+
}
|
|
110
|
+
let inFlight = flushPromise;
|
|
111
|
+
while (inFlight) {
|
|
112
|
+
await inFlight;
|
|
113
|
+
inFlight = flushPromise;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const replayFromDisk = procEnv.BRIDGE_REPLAY_FROM_DISK === "1";
|
|
117
|
+
if (replayFromDisk && existsSync(eventLogPath)) {
|
|
118
|
+
try {
|
|
119
|
+
const lines = readFileSync(eventLogPath, "utf8").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
120
|
+
eventLog = lines.map((line) => ({
|
|
121
|
+
seq: JSON.parse(line).seq,
|
|
122
|
+
line
|
|
123
|
+
}));
|
|
124
|
+
seqCounter = eventLog.at(-1)?.seq ?? 0;
|
|
125
|
+
} catch {
|
|
126
|
+
eventLog = [];
|
|
127
|
+
seqCounter = 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const pendingToolResults = /* @__PURE__ */ new Map();
|
|
131
|
+
const pendingToolApprovals = /* @__PURE__ */ new Map();
|
|
132
|
+
const writeBridgeMeta = async (state) => {
|
|
133
|
+
try {
|
|
134
|
+
await writeFile(
|
|
135
|
+
bridgeMetaPath,
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
type: bridgeType2,
|
|
138
|
+
port: currentBoundPort,
|
|
139
|
+
state,
|
|
140
|
+
pid
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const writeStartConfig = async (start) => {
|
|
147
|
+
try {
|
|
148
|
+
const serialized = JSON.stringify(start);
|
|
149
|
+
await writeFile(startConfigPath, serialized);
|
|
150
|
+
if (!existsSync(rerunStartConfigPath)) {
|
|
151
|
+
await writeFile(rerunStartConfigPath, serialized);
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const sendControl = (msg) => {
|
|
157
|
+
if (activeSocket?.readyState === WS_OPEN) {
|
|
158
|
+
try {
|
|
159
|
+
activeSocket.send(JSON.stringify(msg));
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const emit = (event) => {
|
|
165
|
+
const seq = ++seqCounter;
|
|
166
|
+
const line = JSON.stringify({ ...event, seq });
|
|
167
|
+
eventLog.push({ seq, line });
|
|
168
|
+
diskBuffer += `${line}
|
|
169
|
+
`;
|
|
170
|
+
scheduleEventFlush();
|
|
171
|
+
if (activeSocketReadyForLiveEvents && activeSocket?.readyState === WS_OPEN) {
|
|
172
|
+
try {
|
|
173
|
+
activeSocket.send(line);
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const replay = (ws, afterSeq) => {
|
|
179
|
+
for (const entry of eventLog) {
|
|
180
|
+
if (entry.seq > afterSeq && ws.readyState === WS_OPEN) {
|
|
181
|
+
ws.send(entry.line);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const shouldEmitDebugEvent = (level, subsystem) => {
|
|
186
|
+
if (!debugConfig?.enabled) return false;
|
|
187
|
+
const threshold = debugConfig.level ?? "debug";
|
|
188
|
+
if (DEBUG_LEVEL_WEIGHT[level] > DEBUG_LEVEL_WEIGHT[threshold]) return false;
|
|
189
|
+
return subsystemMatches(debugConfig.subsystems, subsystem);
|
|
190
|
+
};
|
|
191
|
+
const rawStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
192
|
+
const rawStderrWrite = process.stderr.write.bind(process.stderr);
|
|
193
|
+
const writeErrorToStderr = (input) => {
|
|
194
|
+
try {
|
|
195
|
+
const formatted = formatBridgeError(input.error);
|
|
196
|
+
rawStderrWrite(
|
|
197
|
+
`[harness:${bridgeType2}:error] ${input.message}: ${formatted.message}
|
|
198
|
+
`
|
|
199
|
+
);
|
|
200
|
+
if (formatted.stack) {
|
|
201
|
+
rawStderrWrite(`${formatted.stack}
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const emitWarning = (input) => {
|
|
208
|
+
try {
|
|
209
|
+
for (const line of input.message.split("\n")) {
|
|
210
|
+
if (line.trim().length > 0) {
|
|
211
|
+
rawStderrWrite(`[harness:${bridgeType2}:warn] ${line}
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const emitError = (input) => {
|
|
219
|
+
writeErrorToStderr({
|
|
220
|
+
message: input.message ?? "bridge error",
|
|
221
|
+
error: input.error
|
|
222
|
+
});
|
|
223
|
+
emit({ type: "error", error: serialiseError(input.error) });
|
|
224
|
+
};
|
|
225
|
+
const installConsoleCapture = () => {
|
|
226
|
+
if (consoleCaptureInstalled) return;
|
|
227
|
+
consoleCaptureInstalled = true;
|
|
228
|
+
const buffers = {
|
|
229
|
+
stdout: "",
|
|
230
|
+
stderr: ""
|
|
231
|
+
};
|
|
232
|
+
const patch = (stream, raw) => (chunk, encoding, cb) => {
|
|
233
|
+
if (debugConfig?.enabled) {
|
|
234
|
+
try {
|
|
235
|
+
const enc = typeof encoding === "string" ? encoding : "utf8";
|
|
236
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(
|
|
237
|
+
enc
|
|
238
|
+
);
|
|
239
|
+
const combined = buffers[stream] + text.replace(/\r\n/g, "\n");
|
|
240
|
+
const parts = combined.split("\n");
|
|
241
|
+
buffers[stream] = parts.pop() ?? "";
|
|
242
|
+
for (const line of parts) {
|
|
243
|
+
const trimmed = line.replace(/\s+$/, "");
|
|
244
|
+
if (trimmed) {
|
|
245
|
+
emit({
|
|
246
|
+
type: "sandbox-log",
|
|
247
|
+
source: bridgeType2,
|
|
248
|
+
stream,
|
|
249
|
+
line: trimmed
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return raw(
|
|
257
|
+
chunk,
|
|
258
|
+
encoding,
|
|
259
|
+
cb
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
process.stdout.write = patch(
|
|
263
|
+
"stdout",
|
|
264
|
+
rawStdoutWrite
|
|
265
|
+
);
|
|
266
|
+
process.stderr.write = patch(
|
|
267
|
+
"stderr",
|
|
268
|
+
rawStderrWrite
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
const handleInbound = async (msg, ws) => {
|
|
272
|
+
switch (msg.type) {
|
|
273
|
+
case "start": {
|
|
274
|
+
if (activeSocket !== ws) return;
|
|
275
|
+
activeSocketReadyForLiveEvents = true;
|
|
276
|
+
const firstTurn = isFirstTurn;
|
|
277
|
+
isFirstTurn = false;
|
|
278
|
+
eventLog = [];
|
|
279
|
+
diskBuffer = "";
|
|
280
|
+
void writeFile(eventLogPath, "").catch(() => {
|
|
281
|
+
});
|
|
282
|
+
turnAbort = new AbortController();
|
|
283
|
+
currentTurnState = "running";
|
|
284
|
+
void writeStartConfig(msg);
|
|
285
|
+
void writeBridgeMeta("running");
|
|
286
|
+
const startDebug = msg.debug;
|
|
287
|
+
debugConfig = {
|
|
288
|
+
enabled: startDebug?.enabled ?? envDebugEnabled,
|
|
289
|
+
level: startDebug?.level ?? procEnv.HARNESS_DEBUG_LEVEL,
|
|
290
|
+
subsystems: startDebug?.subsystems ?? parseEnvList(procEnv.HARNESS_DEBUG_SUBSYSTEMS)
|
|
291
|
+
};
|
|
292
|
+
if (debugConfig.enabled) {
|
|
293
|
+
installConsoleCapture();
|
|
294
|
+
}
|
|
295
|
+
const turn = {
|
|
296
|
+
emit,
|
|
297
|
+
requestToolResult: (toolCallId) => new Promise((resolve) => {
|
|
298
|
+
pendingToolResults.set(toolCallId, resolve);
|
|
299
|
+
}),
|
|
300
|
+
requestToolApproval: (approvalId) => new Promise((resolve) => {
|
|
301
|
+
pendingToolApprovals.set(approvalId, resolve);
|
|
302
|
+
}),
|
|
303
|
+
pendingUserMessages: [],
|
|
304
|
+
abortSignal: turnAbort.signal,
|
|
305
|
+
firstTurn,
|
|
306
|
+
bridgeLog: (input) => {
|
|
307
|
+
const level = input.level ?? "debug";
|
|
308
|
+
if (!shouldEmitDebugEvent(level, input.subsystem)) return;
|
|
309
|
+
emit({
|
|
310
|
+
type: "debug-event",
|
|
311
|
+
level,
|
|
312
|
+
subsystem: input.subsystem,
|
|
313
|
+
message: input.message,
|
|
314
|
+
...input.attrs ? { attrs: input.attrs } : {},
|
|
315
|
+
...input.error !== void 0 ? { error: formatBridgeError(input.error) } : {}
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
emitWarning,
|
|
319
|
+
emitError
|
|
320
|
+
};
|
|
321
|
+
currentUserMessages = turn.pendingUserMessages;
|
|
322
|
+
try {
|
|
323
|
+
await onStart(msg, turn);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
emitError({ error: err, message: "bridge turn failed" });
|
|
326
|
+
} finally {
|
|
327
|
+
currentTurnState = "waiting";
|
|
328
|
+
void writeBridgeMeta("waiting");
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
case "tool-result": {
|
|
333
|
+
const resolver = pendingToolResults.get(msg.toolCallId);
|
|
334
|
+
if (resolver) {
|
|
335
|
+
pendingToolResults.delete(msg.toolCallId);
|
|
336
|
+
resolver({ output: msg.output, isError: msg.isError });
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
case "tool-approval-response": {
|
|
341
|
+
const resolver = pendingToolApprovals.get(msg.approvalId);
|
|
342
|
+
if (resolver) {
|
|
343
|
+
pendingToolApprovals.delete(msg.approvalId);
|
|
344
|
+
resolver({ approved: msg.approved, reason: msg.reason });
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
case "user-message":
|
|
349
|
+
currentUserMessages?.push(msg.text);
|
|
350
|
+
return;
|
|
351
|
+
case "abort":
|
|
352
|
+
turnAbort?.abort();
|
|
353
|
+
return;
|
|
354
|
+
case "resume":
|
|
355
|
+
if (activeSocket !== ws) return;
|
|
356
|
+
replay(ws, msg.lastSeenEventId);
|
|
357
|
+
activeSocketReadyForLiveEvents = true;
|
|
358
|
+
return;
|
|
359
|
+
case "destroy":
|
|
360
|
+
currentTurnState = "done";
|
|
361
|
+
void writeBridgeMeta("done");
|
|
362
|
+
await onDestroy?.();
|
|
363
|
+
drainThenExit(ws, 1e3, "destroy");
|
|
364
|
+
return;
|
|
365
|
+
case "stop": {
|
|
366
|
+
currentTurnState = "done";
|
|
367
|
+
void writeBridgeMeta("done");
|
|
368
|
+
const data = await onStop?.() ?? {};
|
|
369
|
+
sendControl({ type: "bridge-stop", data });
|
|
370
|
+
drainThenExit(ws, 1e3, "stop");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
void writeBridgeMeta("init");
|
|
376
|
+
const wss = new WebSocketServer({ port: bridgeWsPort, host: "0.0.0.0" });
|
|
377
|
+
const exit = () => {
|
|
378
|
+
if (options.onExit) {
|
|
379
|
+
options.onExit();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
wss.close(() => process.exit(0));
|
|
383
|
+
setTimeout(() => process.exit(0), 1e3).unref();
|
|
384
|
+
};
|
|
385
|
+
const drainThenExit = (ws, code, reason) => {
|
|
386
|
+
const start = Date.now();
|
|
387
|
+
const tick = () => {
|
|
388
|
+
const drained = ws.bufferedAmount === 0 || ws.readyState !== WS_OPEN;
|
|
389
|
+
if (drained || Date.now() - start >= 5e3) {
|
|
390
|
+
void flushPendingEventsToDisk().finally(() => {
|
|
391
|
+
try {
|
|
392
|
+
ws.close(code, reason);
|
|
393
|
+
} finally {
|
|
394
|
+
exit();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
setTimeout(tick, 10).unref();
|
|
400
|
+
};
|
|
401
|
+
tick();
|
|
402
|
+
};
|
|
403
|
+
wss.on("listening", () => {
|
|
404
|
+
const addr = wss.address();
|
|
405
|
+
currentBoundPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
406
|
+
currentTurnState = "waiting";
|
|
407
|
+
void writeBridgeMeta("waiting");
|
|
408
|
+
stdout.write(
|
|
409
|
+
JSON.stringify({
|
|
410
|
+
type: "bridge-ready",
|
|
411
|
+
port: currentBoundPort
|
|
412
|
+
}) + "\n"
|
|
413
|
+
);
|
|
414
|
+
options.onListening?.(currentBoundPort);
|
|
415
|
+
});
|
|
416
|
+
wss.on("connection", (ws, req) => {
|
|
417
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
418
|
+
if (url.searchParams.get("agent_bridge_token") !== expectedToken) {
|
|
419
|
+
ws.close(1008, "unauthorized");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
activeSocket = ws;
|
|
423
|
+
activeSocketReadyForLiveEvents = false;
|
|
424
|
+
sendControl({
|
|
425
|
+
type: "bridge-hello",
|
|
426
|
+
state: currentTurnState,
|
|
427
|
+
lastSeq: seqCounter
|
|
428
|
+
});
|
|
429
|
+
ws.on("message", (raw) => {
|
|
430
|
+
let parsed;
|
|
431
|
+
try {
|
|
432
|
+
const text = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
|
|
433
|
+
parsed = JSON.parse(text);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
sendControl({
|
|
436
|
+
type: "error",
|
|
437
|
+
error: `protocol parse error: ${err.message}`
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
void handleInbound(parsed, ws);
|
|
442
|
+
});
|
|
443
|
+
ws.on("close", () => {
|
|
444
|
+
if (activeSocket === ws) {
|
|
445
|
+
activeSocket = void 0;
|
|
446
|
+
activeSocketReadyForLiveEvents = false;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
ws.on("error", () => {
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
process.on("uncaughtException", (err) => {
|
|
453
|
+
emitError({ error: err, message: "uncaught exception" });
|
|
454
|
+
});
|
|
455
|
+
process.on("unhandledRejection", (err) => {
|
|
456
|
+
emitError({ error: err, message: "unhandled rejection" });
|
|
457
|
+
});
|
|
458
|
+
await new Promise((resolve, reject) => {
|
|
459
|
+
if (wss.address() != null) {
|
|
460
|
+
resolve();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
wss.once("listening", resolve);
|
|
464
|
+
wss.once("error", reject);
|
|
465
|
+
});
|
|
466
|
+
return {
|
|
467
|
+
port: currentBoundPort,
|
|
468
|
+
close: () => new Promise((resolve) => {
|
|
469
|
+
wss.close(() => resolve());
|
|
470
|
+
})
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function serialiseError(err) {
|
|
474
|
+
if (err instanceof Error) {
|
|
475
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
476
|
+
}
|
|
477
|
+
return err;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/v1/bridge/index.ts
|
|
481
|
+
import * as acp4 from "@agentclientprotocol/sdk";
|
|
482
|
+
import { spawn } from "child_process";
|
|
483
|
+
import { readFile, writeFile as writeFile2 } from "fs/promises";
|
|
484
|
+
import { fileURLToPath } from "url";
|
|
485
|
+
import { Readable, Writable } from "stream";
|
|
486
|
+
import { argv, env as processEnv } from "process";
|
|
487
|
+
|
|
488
|
+
// src/v1/bridge/acp-v1-bridge-environment.ts
|
|
489
|
+
import { z } from "zod/v4";
|
|
490
|
+
var ACP_BRIDGE_CONFIGURATION_ENV = "AI_SDK_ACP_BRIDGE_CONFIGURATION";
|
|
491
|
+
var serializableValueSchema = z.json();
|
|
492
|
+
var profileValueSchema = z.lazy(
|
|
493
|
+
() => z.union([
|
|
494
|
+
z.string(),
|
|
495
|
+
z.number(),
|
|
496
|
+
z.boolean(),
|
|
497
|
+
z.null(),
|
|
498
|
+
z.object({
|
|
499
|
+
$source: z.enum([
|
|
500
|
+
"gateway-api-key",
|
|
501
|
+
"gateway-base-url",
|
|
502
|
+
"gateway-authorization",
|
|
503
|
+
"client-app",
|
|
504
|
+
"client-app-name",
|
|
505
|
+
"client-app-version"
|
|
506
|
+
]),
|
|
507
|
+
prefix: z.string().optional(),
|
|
508
|
+
suffix: z.string().optional(),
|
|
509
|
+
ensureSuffix: z.string().optional()
|
|
510
|
+
}),
|
|
511
|
+
z.array(profileValueSchema),
|
|
512
|
+
z.record(z.string(), profileValueSchema)
|
|
513
|
+
])
|
|
514
|
+
);
|
|
515
|
+
var serializableRecordSchema = z.record(z.string(), serializableValueSchema);
|
|
516
|
+
var profileRecordSchema = z.record(z.string(), profileValueSchema);
|
|
517
|
+
var authenticationSchema = z.object({
|
|
518
|
+
methodId: z.string(),
|
|
519
|
+
meta: serializableRecordSchema.optional(),
|
|
520
|
+
clientCapabilities: serializableRecordSchema.optional()
|
|
521
|
+
});
|
|
522
|
+
var providerAuthenticationSchema = z.discriminatedUnion("type", [
|
|
523
|
+
z.object({ type: z.literal("direct") }),
|
|
524
|
+
z.object({
|
|
525
|
+
type: z.literal("ai-gateway"),
|
|
526
|
+
env: profileRecordSchema
|
|
527
|
+
})
|
|
528
|
+
]);
|
|
529
|
+
var bridgeConfigurationSchema = z.object({
|
|
530
|
+
authentication: authenticationSchema.optional(),
|
|
531
|
+
providerAuthentication: providerAuthenticationSchema.optional(),
|
|
532
|
+
sessionMeta: serializableRecordSchema.optional()
|
|
533
|
+
});
|
|
534
|
+
async function readACPBridgeEnvironment({
|
|
535
|
+
env
|
|
536
|
+
}) {
|
|
537
|
+
const serialized = env[ACP_BRIDGE_CONFIGURATION_ENV];
|
|
538
|
+
if (serialized == null) return {};
|
|
539
|
+
try {
|
|
540
|
+
const result = bridgeConfigurationSchema.safeParse(JSON.parse(serialized));
|
|
541
|
+
if (result.success) return result.data;
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
throw new Error("ACP bridge configuration environment is invalid.");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/v1/bridge/profile-values.ts
|
|
548
|
+
function resolveACPProfileValue({
|
|
549
|
+
value,
|
|
550
|
+
gateway
|
|
551
|
+
}) {
|
|
552
|
+
if (Array.isArray(value)) {
|
|
553
|
+
return value.map((item) => resolveACPProfileValue({ value: item, gateway }));
|
|
554
|
+
}
|
|
555
|
+
if (value !== null && typeof value === "object") {
|
|
556
|
+
if (isValueSource(value)) {
|
|
557
|
+
const sourceValue = resolveSource({ source: value.$source, gateway });
|
|
558
|
+
const resolved = `${value.prefix ?? ""}${sourceValue}`;
|
|
559
|
+
return value.ensureSuffix == null ? `${resolved}${value.suffix ?? ""}` : ensureSuffix({
|
|
560
|
+
value: resolved,
|
|
561
|
+
suffix: value.ensureSuffix
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return Object.fromEntries(
|
|
565
|
+
Object.entries(value).map(([key, item]) => [
|
|
566
|
+
key,
|
|
567
|
+
resolveACPProfileValue({ value: item, gateway })
|
|
568
|
+
])
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
return value;
|
|
572
|
+
}
|
|
573
|
+
function isValueSource(value) {
|
|
574
|
+
return "$source" in value && typeof value.$source === "string";
|
|
575
|
+
}
|
|
576
|
+
function ensureSuffix({
|
|
577
|
+
value,
|
|
578
|
+
suffix
|
|
579
|
+
}) {
|
|
580
|
+
const normalized = value.replace(/\/+$/, "");
|
|
581
|
+
return normalized.endsWith(suffix) ? normalized : `${normalized}${suffix}`;
|
|
582
|
+
}
|
|
583
|
+
function resolveSource({
|
|
584
|
+
source,
|
|
585
|
+
gateway
|
|
586
|
+
}) {
|
|
587
|
+
switch (source) {
|
|
588
|
+
case "gateway-api-key":
|
|
589
|
+
return gateway.apiKey;
|
|
590
|
+
case "gateway-base-url":
|
|
591
|
+
return gateway.baseUrl;
|
|
592
|
+
case "gateway-authorization":
|
|
593
|
+
return `Bearer ${gateway.apiKey}`;
|
|
594
|
+
case "client-app":
|
|
595
|
+
return `${gateway.clientAppName}/${gateway.clientAppVersion}`;
|
|
596
|
+
case "client-app-name":
|
|
597
|
+
return gateway.clientAppName;
|
|
598
|
+
case "client-app-version":
|
|
599
|
+
return gateway.clientAppVersion;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/v1/bridge/protocol-configuration.ts
|
|
604
|
+
function createACPInitializeRequest({
|
|
605
|
+
protocolVersion,
|
|
606
|
+
clientApp,
|
|
607
|
+
authentication,
|
|
608
|
+
supportsBooleanSessionConfigOptions = false
|
|
609
|
+
}) {
|
|
610
|
+
let clientCapabilities = authentication?.clientCapabilities ?? {};
|
|
611
|
+
if (supportsBooleanSessionConfigOptions) {
|
|
612
|
+
clientCapabilities = mergeRecords({
|
|
613
|
+
left: clientCapabilities,
|
|
614
|
+
right: {
|
|
615
|
+
session: {
|
|
616
|
+
configOptions: {
|
|
617
|
+
boolean: {}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
protocolVersion,
|
|
625
|
+
clientInfo: clientApp,
|
|
626
|
+
clientCapabilities
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function resolveACPLaunchEnvironment({
|
|
630
|
+
providerAuthentication,
|
|
631
|
+
gateway
|
|
632
|
+
}) {
|
|
633
|
+
if (providerAuthentication?.type !== "ai-gateway") {
|
|
634
|
+
return {};
|
|
635
|
+
}
|
|
636
|
+
const resolved = asRecord(
|
|
637
|
+
resolveACPProfileValue({
|
|
638
|
+
value: providerAuthentication.env,
|
|
639
|
+
gateway: requireGateway({ gateway })
|
|
640
|
+
})
|
|
641
|
+
);
|
|
642
|
+
return Object.fromEntries(
|
|
643
|
+
Object.entries(resolved).map(([key, value]) => [
|
|
644
|
+
key,
|
|
645
|
+
typeof value === "string" ? value : JSON.stringify(value)
|
|
646
|
+
])
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
function validateACPProtocolVersion({
|
|
650
|
+
requested,
|
|
651
|
+
initialization
|
|
652
|
+
}) {
|
|
653
|
+
if (initialization.protocolVersion !== requested) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`ACP protocol negotiation failed: requested v${requested}, agent selected v${initialization.protocolVersion}.`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function assertACPAuthenticationMethod({
|
|
660
|
+
initialization,
|
|
661
|
+
methodId
|
|
662
|
+
}) {
|
|
663
|
+
if (!initialization.authMethods?.some((method) => method.id === methodId)) {
|
|
664
|
+
const advertised = initialization.authMethods?.map((method) => method.id).join(", ") || "none";
|
|
665
|
+
throw new Error(
|
|
666
|
+
`ACP authentication method ${JSON.stringify(methodId)} is not advertised by the agent. Advertised methods: ${advertised}.`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function mergeRecords({
|
|
671
|
+
left,
|
|
672
|
+
right
|
|
673
|
+
}) {
|
|
674
|
+
const result = { ...left };
|
|
675
|
+
for (const [key, value] of Object.entries(right)) {
|
|
676
|
+
const previous = result[key];
|
|
677
|
+
result[key] = isRecord(previous) && isRecord(value) ? mergeRecords({ left: previous, right: value }) : value;
|
|
678
|
+
}
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
function requireGateway({
|
|
682
|
+
gateway
|
|
683
|
+
}) {
|
|
684
|
+
if (gateway == null) {
|
|
685
|
+
throw new Error("ACP Gateway profile values are unavailable.");
|
|
686
|
+
}
|
|
687
|
+
return gateway;
|
|
688
|
+
}
|
|
689
|
+
function asRecord(value) {
|
|
690
|
+
if (!isRecord(value)) {
|
|
691
|
+
throw new Error("ACP profile data must resolve to an object.");
|
|
692
|
+
}
|
|
693
|
+
return value;
|
|
694
|
+
}
|
|
695
|
+
function isRecord(value) {
|
|
696
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/v1/bridge/acp-stream-capture.ts
|
|
700
|
+
var KNOWN_SESSION_UPDATES = /* @__PURE__ */ new Set([
|
|
701
|
+
"user_message_chunk",
|
|
702
|
+
"agent_message_chunk",
|
|
703
|
+
"agent_thought_chunk",
|
|
704
|
+
"tool_call",
|
|
705
|
+
"tool_call_update",
|
|
706
|
+
"plan",
|
|
707
|
+
"plan_update",
|
|
708
|
+
"plan_removed",
|
|
709
|
+
"available_commands_update",
|
|
710
|
+
"current_mode_update",
|
|
711
|
+
"config_option_update",
|
|
712
|
+
"session_info_update",
|
|
713
|
+
"usage_update"
|
|
714
|
+
]);
|
|
715
|
+
function captureACPStream({ stream }) {
|
|
716
|
+
const updates = [];
|
|
717
|
+
const readable = stream.readable.pipeThrough(
|
|
718
|
+
new TransformStream({
|
|
719
|
+
transform(message, controller) {
|
|
720
|
+
const rawUpdate = getRawSessionUpdate({ message });
|
|
721
|
+
if (!rawUpdate.isSessionUpdate) {
|
|
722
|
+
controller.enqueue(message);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const sessionUpdate = getStringProperty({
|
|
726
|
+
value: rawUpdate.value,
|
|
727
|
+
property: "sessionUpdate"
|
|
728
|
+
});
|
|
729
|
+
const forwarded = !rawUpdate.canFilterUnknown || sessionUpdate == null || KNOWN_SESSION_UPDATES.has(sessionUpdate);
|
|
730
|
+
updates.push({ rawUpdate: rawUpdate.value, forwarded });
|
|
731
|
+
if (forwarded) controller.enqueue(message);
|
|
732
|
+
}
|
|
733
|
+
})
|
|
734
|
+
);
|
|
735
|
+
return {
|
|
736
|
+
stream: { ...stream, readable },
|
|
737
|
+
capture: {
|
|
738
|
+
takeForUpdate: ({ update }) => {
|
|
739
|
+
const index = updates.findIndex(
|
|
740
|
+
(candidate) => candidate.forwarded && sessionUpdatesMatch({
|
|
741
|
+
rawUpdate: candidate.rawUpdate,
|
|
742
|
+
update
|
|
743
|
+
})
|
|
744
|
+
);
|
|
745
|
+
if (index === -1) {
|
|
746
|
+
return { precedingRawValues: [], rawUpdate: update };
|
|
747
|
+
}
|
|
748
|
+
const consumed = updates.splice(0, index + 1);
|
|
749
|
+
return {
|
|
750
|
+
precedingRawValues: consumed.slice(0, -1).map((candidate) => candidate.rawUpdate),
|
|
751
|
+
rawUpdate: consumed.at(-1).rawUpdate
|
|
752
|
+
};
|
|
753
|
+
},
|
|
754
|
+
drainRawValues: () => updates.splice(0).map((candidate) => candidate.rawUpdate)
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function getRawSessionUpdate({ message }) {
|
|
759
|
+
const record = message;
|
|
760
|
+
if (record.method !== "session/update" || !isRecord2(record.params)) {
|
|
761
|
+
return { isSessionUpdate: false };
|
|
762
|
+
}
|
|
763
|
+
const params = record.params;
|
|
764
|
+
return {
|
|
765
|
+
isSessionUpdate: true,
|
|
766
|
+
value: params.update,
|
|
767
|
+
canFilterUnknown: typeof params.sessionId === "string" && isRecord2(params.update)
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function sessionUpdatesMatch({
|
|
771
|
+
rawUpdate,
|
|
772
|
+
update
|
|
773
|
+
}) {
|
|
774
|
+
if (!isRecord2(rawUpdate)) return false;
|
|
775
|
+
if (rawUpdate.sessionUpdate !== update.sessionUpdate) return false;
|
|
776
|
+
if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
|
|
777
|
+
return rawUpdate.toolCallId === update.toolCallId;
|
|
778
|
+
}
|
|
779
|
+
if (update.sessionUpdate === "user_message_chunk" || update.sessionUpdate === "agent_message_chunk" || update.sessionUpdate === "agent_thought_chunk") {
|
|
780
|
+
return rawUpdate.messageId === update.messageId;
|
|
781
|
+
}
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
function getStringProperty({
|
|
785
|
+
value,
|
|
786
|
+
property
|
|
787
|
+
}) {
|
|
788
|
+
if (!isRecord2(value)) return void 0;
|
|
789
|
+
const propertyValue = value[property];
|
|
790
|
+
return typeof propertyValue === "string" ? propertyValue : void 0;
|
|
791
|
+
}
|
|
792
|
+
function isRecord2(value) {
|
|
793
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/v1/bridge/acp-diagnostics.ts
|
|
797
|
+
function createACPInitializationDiagnostic({
|
|
798
|
+
initialization,
|
|
799
|
+
sessionId
|
|
800
|
+
}) {
|
|
801
|
+
const agentInfo = initialization.agentInfo;
|
|
802
|
+
return {
|
|
803
|
+
protocolVersion: initialization.protocolVersion,
|
|
804
|
+
sessionId,
|
|
805
|
+
agent: agentInfo == null ? null : {
|
|
806
|
+
name: agentInfo.name,
|
|
807
|
+
version: agentInfo.version,
|
|
808
|
+
...agentInfo.title == null ? {} : { title: agentInfo.title }
|
|
809
|
+
},
|
|
810
|
+
capabilities: stripMetadata({
|
|
811
|
+
value: initialization.agentCapabilities ?? {}
|
|
812
|
+
}),
|
|
813
|
+
authMethods: (initialization.authMethods ?? []).map((method) => ({
|
|
814
|
+
id: method.id,
|
|
815
|
+
type: "type" in method ? method.type : "agent"
|
|
816
|
+
}))
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function createACPBridgeError({
|
|
820
|
+
stage,
|
|
821
|
+
cause
|
|
822
|
+
}) {
|
|
823
|
+
const error = new Error(`ACP ${stage} failed.`);
|
|
824
|
+
error.cause = cause;
|
|
825
|
+
return error;
|
|
826
|
+
}
|
|
827
|
+
function stripMetadata({ value }) {
|
|
828
|
+
if (Array.isArray(value)) {
|
|
829
|
+
return value.map((item) => stripMetadata({ value: item }));
|
|
830
|
+
}
|
|
831
|
+
if (value == null || typeof value !== "object") return value;
|
|
832
|
+
const result = {};
|
|
833
|
+
for (const [key, item] of Object.entries(value)) {
|
|
834
|
+
if (key !== "_meta") result[key] = stripMetadata({ value: item });
|
|
835
|
+
}
|
|
836
|
+
return result;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/v1/bridge/stream-translator.ts
|
|
840
|
+
function createACPStreamTranslator({
|
|
841
|
+
emit,
|
|
842
|
+
builtinTools = []
|
|
843
|
+
}) {
|
|
844
|
+
let openBlock;
|
|
845
|
+
let blockCounter = 0;
|
|
846
|
+
let stepOpen = false;
|
|
847
|
+
let completedToolStepEligible = false;
|
|
848
|
+
let finished = false;
|
|
849
|
+
const blockIdCounts = /* @__PURE__ */ new Map();
|
|
850
|
+
const toolStates = /* @__PURE__ */ new Map();
|
|
851
|
+
const pendingToolCallIds = /* @__PURE__ */ new Set();
|
|
852
|
+
const builtinToolsByName = indexBuiltinTools({ builtinTools });
|
|
853
|
+
const closeBlock = () => {
|
|
854
|
+
if (openBlock == null) return;
|
|
855
|
+
emit({
|
|
856
|
+
type: openBlock.type === "text" ? "text-end" : "reasoning-end",
|
|
857
|
+
id: openBlock.id
|
|
858
|
+
});
|
|
859
|
+
openBlock = void 0;
|
|
860
|
+
};
|
|
861
|
+
const createBlockId = ({
|
|
862
|
+
type,
|
|
863
|
+
messageId
|
|
864
|
+
}) => {
|
|
865
|
+
const base = messageId ?? `acp-${type}-${++blockCounter}`;
|
|
866
|
+
const count = (blockIdCounts.get(base) ?? 0) + 1;
|
|
867
|
+
blockIdCounts.set(base, count);
|
|
868
|
+
return count === 1 ? base : `${base}-${count}`;
|
|
869
|
+
};
|
|
870
|
+
const emitContent = ({
|
|
871
|
+
type,
|
|
872
|
+
text,
|
|
873
|
+
messageId
|
|
874
|
+
}) => {
|
|
875
|
+
const normalizedMessageId = messageId ?? null;
|
|
876
|
+
if (openBlock == null || openBlock.type !== type || openBlock.messageId !== normalizedMessageId) {
|
|
877
|
+
closeBlock();
|
|
878
|
+
const id = createBlockId({ type, messageId: normalizedMessageId });
|
|
879
|
+
emit({
|
|
880
|
+
type: type === "text" ? "text-start" : "reasoning-start",
|
|
881
|
+
id
|
|
882
|
+
});
|
|
883
|
+
openBlock = { type, id, messageId: normalizedMessageId };
|
|
884
|
+
}
|
|
885
|
+
emit({
|
|
886
|
+
type: type === "text" ? "text-delta" : "reasoning-delta",
|
|
887
|
+
id: openBlock.id,
|
|
888
|
+
delta: text
|
|
889
|
+
});
|
|
890
|
+
stepOpen = true;
|
|
891
|
+
};
|
|
892
|
+
const emitInferredStep = ({
|
|
893
|
+
finishReason
|
|
894
|
+
}) => {
|
|
895
|
+
closeBlock();
|
|
896
|
+
emit({
|
|
897
|
+
type: "finish-step",
|
|
898
|
+
finishReason,
|
|
899
|
+
usage: unknownUsage(),
|
|
900
|
+
harnessMetadata: { acp: { inferredStep: true } }
|
|
901
|
+
});
|
|
902
|
+
stepOpen = false;
|
|
903
|
+
completedToolStepEligible = false;
|
|
904
|
+
};
|
|
905
|
+
const flushCompletedToolStep = () => {
|
|
906
|
+
if (!stepOpen || !completedToolStepEligible || pendingToolCallIds.size > 0) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
emitInferredStep({
|
|
910
|
+
finishReason: { unified: "tool-calls", raw: "tool-calls" }
|
|
911
|
+
});
|
|
912
|
+
};
|
|
913
|
+
const emitToolUpdate = ({
|
|
914
|
+
update,
|
|
915
|
+
rawUpdate,
|
|
916
|
+
forceEmit = false
|
|
917
|
+
}) => {
|
|
918
|
+
closeBlock();
|
|
919
|
+
const state = toolStates.get(update.toolCallId) ?? {
|
|
920
|
+
toolCallId: update.toolCallId,
|
|
921
|
+
values: {},
|
|
922
|
+
emittedFilePaths: /* @__PURE__ */ new Set(),
|
|
923
|
+
emittedCall: false,
|
|
924
|
+
emittedResult: false
|
|
925
|
+
};
|
|
926
|
+
toolStates.set(update.toolCallId, state);
|
|
927
|
+
mergeToolUpdate({ state, update, rawUpdate });
|
|
928
|
+
if (!forceEmit && !state.emittedCall && update.sessionUpdate === "tool_call" && update.status === "pending") {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (!state.emittedCall) {
|
|
932
|
+
const programmaticName = getStringProperty2({
|
|
933
|
+
value: state.values,
|
|
934
|
+
property: "name"
|
|
935
|
+
});
|
|
936
|
+
const builtin = resolveBuiltinTool({
|
|
937
|
+
programmaticName,
|
|
938
|
+
metadata: state.values._meta,
|
|
939
|
+
rawInput: state.values.rawInput,
|
|
940
|
+
builtinTools,
|
|
941
|
+
builtinToolsByName
|
|
942
|
+
});
|
|
943
|
+
if (!forceEmit && builtin?.inputSchema != null && state.values.status !== "completed" && state.values.status !== "failed" && !hasRequiredBuiltinToolInput({
|
|
944
|
+
rawInput: state.values.rawInput,
|
|
945
|
+
inputSchema: builtin.inputSchema
|
|
946
|
+
})) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (builtin != null) {
|
|
950
|
+
state.toolName = builtin.toolName;
|
|
951
|
+
state.nativeName = builtin.nativeName != null && builtin.toolName !== builtin.nativeName ? builtin.nativeName : void 0;
|
|
952
|
+
state.dynamic = false;
|
|
953
|
+
} else {
|
|
954
|
+
state.toolName = createDynamicToolName({
|
|
955
|
+
programmaticName,
|
|
956
|
+
toolCallId: state.toolCallId
|
|
957
|
+
});
|
|
958
|
+
state.dynamic = true;
|
|
959
|
+
}
|
|
960
|
+
emit({
|
|
961
|
+
type: "tool-call",
|
|
962
|
+
toolCallId: state.toolCallId,
|
|
963
|
+
toolName: state.toolName,
|
|
964
|
+
input: stringifyToolInput({ input: state.values.rawInput }),
|
|
965
|
+
providerExecuted: true,
|
|
966
|
+
...state.dynamic ? { dynamic: true } : {},
|
|
967
|
+
...state.nativeName == null ? {} : { nativeName: state.nativeName }
|
|
968
|
+
});
|
|
969
|
+
state.emittedCall = true;
|
|
970
|
+
stepOpen = true;
|
|
971
|
+
pendingToolCallIds.add(state.toolCallId);
|
|
972
|
+
completedToolStepEligible = false;
|
|
973
|
+
}
|
|
974
|
+
emitFileChanges({ state, emit });
|
|
975
|
+
if (!state.emittedResult && (state.values.status === "completed" || state.values.status === "failed")) {
|
|
976
|
+
emit({
|
|
977
|
+
type: "tool-result",
|
|
978
|
+
toolCallId: state.toolCallId,
|
|
979
|
+
toolName: state.toolName,
|
|
980
|
+
result: createToolResult({ state }),
|
|
981
|
+
...state.values.status === "failed" ? { isError: true } : {},
|
|
982
|
+
...state.dynamic ? { dynamic: true } : {}
|
|
983
|
+
});
|
|
984
|
+
state.emittedResult = true;
|
|
985
|
+
pendingToolCallIds.delete(state.toolCallId);
|
|
986
|
+
completedToolStepEligible = pendingToolCallIds.size === 0;
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
const hostToolCall = ({
|
|
990
|
+
toolCallId,
|
|
991
|
+
toolName,
|
|
992
|
+
input
|
|
993
|
+
}) => {
|
|
994
|
+
closeBlock();
|
|
995
|
+
emit({
|
|
996
|
+
type: "tool-call",
|
|
997
|
+
toolCallId,
|
|
998
|
+
toolName,
|
|
999
|
+
input: stringifyToolInput({ input }),
|
|
1000
|
+
providerExecuted: false
|
|
1001
|
+
});
|
|
1002
|
+
stepOpen = true;
|
|
1003
|
+
pendingToolCallIds.add(toolCallId);
|
|
1004
|
+
completedToolStepEligible = false;
|
|
1005
|
+
};
|
|
1006
|
+
const hostToolResult = ({
|
|
1007
|
+
toolCallId,
|
|
1008
|
+
toolName,
|
|
1009
|
+
output,
|
|
1010
|
+
isError
|
|
1011
|
+
}) => {
|
|
1012
|
+
closeBlock();
|
|
1013
|
+
emit({
|
|
1014
|
+
type: "tool-result",
|
|
1015
|
+
toolCallId,
|
|
1016
|
+
toolName,
|
|
1017
|
+
result: toSafeJSONValue({ value: output, fallback: {} }) ?? {},
|
|
1018
|
+
...isError ? { isError: true } : {}
|
|
1019
|
+
});
|
|
1020
|
+
pendingToolCallIds.delete(toolCallId);
|
|
1021
|
+
completedToolStepEligible = pendingToolCallIds.size === 0;
|
|
1022
|
+
};
|
|
1023
|
+
return {
|
|
1024
|
+
update: (options) => {
|
|
1025
|
+
const { update, rawUpdate, preserveRaw } = "update" in options ? {
|
|
1026
|
+
update: options.update,
|
|
1027
|
+
rawUpdate: options.rawUpdate,
|
|
1028
|
+
preserveRaw: options.preserveRaw
|
|
1029
|
+
} : { update: options, rawUpdate: options, preserveRaw: true };
|
|
1030
|
+
const preservedUpdate = rawUpdate === void 0 ? update : rawUpdate;
|
|
1031
|
+
if (preserveRaw !== false) {
|
|
1032
|
+
emit({ type: "raw", rawValue: preservedUpdate });
|
|
1033
|
+
}
|
|
1034
|
+
if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && typeof update.content.text === "string") {
|
|
1035
|
+
flushCompletedToolStep();
|
|
1036
|
+
emitContent({
|
|
1037
|
+
type: "text",
|
|
1038
|
+
text: update.content.text,
|
|
1039
|
+
messageId: update.messageId
|
|
1040
|
+
});
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (update.sessionUpdate === "agent_thought_chunk" && update.content?.type === "text" && typeof update.content.text === "string") {
|
|
1044
|
+
flushCompletedToolStep();
|
|
1045
|
+
emitContent({
|
|
1046
|
+
type: "reasoning",
|
|
1047
|
+
text: update.content.text,
|
|
1048
|
+
messageId: update.messageId
|
|
1049
|
+
});
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (update.sessionUpdate === "agent_message_chunk" || update.sessionUpdate === "agent_thought_chunk") {
|
|
1053
|
+
closeBlock();
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
|
|
1057
|
+
emitToolUpdate({ update, rawUpdate: preservedUpdate });
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
permissionToolCall: ({ toolCall }) => {
|
|
1061
|
+
emitToolUpdate({
|
|
1062
|
+
update: toolCall,
|
|
1063
|
+
rawUpdate: toolCall,
|
|
1064
|
+
forceEmit: true
|
|
1065
|
+
});
|
|
1066
|
+
},
|
|
1067
|
+
raw: ({ rawValue }) => {
|
|
1068
|
+
emit({ type: "raw", rawValue });
|
|
1069
|
+
},
|
|
1070
|
+
close: closeBlock,
|
|
1071
|
+
hostToolCall,
|
|
1072
|
+
hostToolResult,
|
|
1073
|
+
finish: (response) => {
|
|
1074
|
+
if (finished) return;
|
|
1075
|
+
finished = true;
|
|
1076
|
+
emit({ type: "raw", rawValue: response });
|
|
1077
|
+
const finishReason = mapACPFinishReason({
|
|
1078
|
+
stopReason: response.stopReason
|
|
1079
|
+
});
|
|
1080
|
+
const usageMetadata = response.usage == null ? void 0 : mapACPUsageMetadata({ usage: response.usage });
|
|
1081
|
+
if (stepOpen) {
|
|
1082
|
+
emitInferredStep({ finishReason });
|
|
1083
|
+
} else {
|
|
1084
|
+
closeBlock();
|
|
1085
|
+
}
|
|
1086
|
+
emit({
|
|
1087
|
+
type: "finish",
|
|
1088
|
+
finishReason,
|
|
1089
|
+
totalUsage: mapACPUsage({ usage: response.usage }),
|
|
1090
|
+
harnessMetadata: {
|
|
1091
|
+
acp: {
|
|
1092
|
+
stopReason: response.stopReason,
|
|
1093
|
+
...usageMetadata == null ? {} : { usage: usageMetadata }
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function mapACPFinishReason({ stopReason }) {
|
|
1101
|
+
switch (stopReason) {
|
|
1102
|
+
case "end_turn":
|
|
1103
|
+
return { unified: "stop", raw: stopReason };
|
|
1104
|
+
case "max_tokens":
|
|
1105
|
+
case "max_turn_requests":
|
|
1106
|
+
return { unified: "length", raw: stopReason };
|
|
1107
|
+
case "refusal":
|
|
1108
|
+
return { unified: "content-filter", raw: stopReason };
|
|
1109
|
+
default:
|
|
1110
|
+
return { unified: "other", raw: stopReason };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function indexBuiltinTools({
|
|
1114
|
+
builtinTools
|
|
1115
|
+
}) {
|
|
1116
|
+
const matches = /* @__PURE__ */ new Map();
|
|
1117
|
+
for (const builtinTool of builtinTools) {
|
|
1118
|
+
addBuiltinToolMatch({
|
|
1119
|
+
matches,
|
|
1120
|
+
name: builtinTool.toolName,
|
|
1121
|
+
builtinTool
|
|
1122
|
+
});
|
|
1123
|
+
if (builtinTool.nativeName != null) {
|
|
1124
|
+
addBuiltinToolMatch({
|
|
1125
|
+
matches,
|
|
1126
|
+
name: builtinTool.nativeName,
|
|
1127
|
+
builtinTool
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return new Map(
|
|
1132
|
+
[...matches].filter(
|
|
1133
|
+
(entry) => entry[1] != null
|
|
1134
|
+
)
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
function addBuiltinToolMatch({
|
|
1138
|
+
matches,
|
|
1139
|
+
name: name2,
|
|
1140
|
+
builtinTool
|
|
1141
|
+
}) {
|
|
1142
|
+
if (!matches.has(name2)) {
|
|
1143
|
+
matches.set(name2, builtinTool);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (matches.get(name2)?.toolName !== builtinTool.toolName) {
|
|
1147
|
+
matches.set(name2, null);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function resolveBuiltinTool({
|
|
1151
|
+
programmaticName,
|
|
1152
|
+
metadata,
|
|
1153
|
+
rawInput,
|
|
1154
|
+
builtinTools,
|
|
1155
|
+
builtinToolsByName
|
|
1156
|
+
}) {
|
|
1157
|
+
if (programmaticName != null) {
|
|
1158
|
+
return builtinToolsByName.get(programmaticName);
|
|
1159
|
+
}
|
|
1160
|
+
const metadataMatch = findBuiltinToolsInMetadata({
|
|
1161
|
+
metadata,
|
|
1162
|
+
builtinToolsByName
|
|
1163
|
+
});
|
|
1164
|
+
if (metadataMatch.hasProgrammaticName) {
|
|
1165
|
+
return metadataMatch.matches.length === 1 ? metadataMatch.matches[0] : void 0;
|
|
1166
|
+
}
|
|
1167
|
+
const schemaMatches = builtinTools.filter(
|
|
1168
|
+
(tool) => matchesBuiltinToolInput({ rawInput, inputSchema: tool.inputSchema })
|
|
1169
|
+
);
|
|
1170
|
+
return schemaMatches.length === 1 ? schemaMatches[0] : void 0;
|
|
1171
|
+
}
|
|
1172
|
+
function findBuiltinToolsInMetadata({
|
|
1173
|
+
metadata,
|
|
1174
|
+
builtinToolsByName
|
|
1175
|
+
}) {
|
|
1176
|
+
const matches = /* @__PURE__ */ new Map();
|
|
1177
|
+
let hasProgrammaticName = false;
|
|
1178
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1179
|
+
const visit = (value) => {
|
|
1180
|
+
if (value == null || typeof value !== "object" || seen.has(value)) return;
|
|
1181
|
+
seen.add(value);
|
|
1182
|
+
for (const [property, child2] of Array.isArray(value) ? value.entries() : Object.entries(value)) {
|
|
1183
|
+
if ((property === "name" || property === "toolName") && typeof child2 === "string") {
|
|
1184
|
+
hasProgrammaticName = true;
|
|
1185
|
+
const builtin = builtinToolsByName.get(child2);
|
|
1186
|
+
if (builtin != null) matches.set(builtin.toolName, builtin);
|
|
1187
|
+
}
|
|
1188
|
+
visit(child2);
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
visit(metadata);
|
|
1192
|
+
return { hasProgrammaticName, matches: [...matches.values()] };
|
|
1193
|
+
}
|
|
1194
|
+
function matchesBuiltinToolInput({
|
|
1195
|
+
rawInput,
|
|
1196
|
+
inputSchema
|
|
1197
|
+
}) {
|
|
1198
|
+
if (!isRecord3(rawInput) || !isRecord3(inputSchema)) return false;
|
|
1199
|
+
if (inputSchema.type !== "object") return false;
|
|
1200
|
+
const required = inputSchema.required;
|
|
1201
|
+
if (!Array.isArray(required) || required.length === 0 || !required.every((property) => typeof property === "string")) {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
if (!hasRequiredBuiltinToolInput({ rawInput, inputSchema })) return false;
|
|
1205
|
+
const properties = isRecord3(inputSchema.properties) ? inputSchema.properties : {};
|
|
1206
|
+
return Object.entries(properties).filter(([, schema]) => isJSONSchemaDiscriminator({ schema })).every(([property, schema]) => {
|
|
1207
|
+
if (!Object.prototype.hasOwnProperty.call(rawInput, property)) {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
return matchesJSONSchemaValue({ value: rawInput[property], schema });
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
function hasRequiredBuiltinToolInput({
|
|
1214
|
+
rawInput,
|
|
1215
|
+
inputSchema
|
|
1216
|
+
}) {
|
|
1217
|
+
if (!isRecord3(inputSchema) || inputSchema.type !== "object") return true;
|
|
1218
|
+
if (!isRecord3(rawInput)) return false;
|
|
1219
|
+
const required = inputSchema.required;
|
|
1220
|
+
if (!Array.isArray(required) || required.length === 0) return true;
|
|
1221
|
+
if (!required.every((property) => typeof property === "string")) return true;
|
|
1222
|
+
const properties = isRecord3(inputSchema.properties) ? inputSchema.properties : {};
|
|
1223
|
+
return required.every((property) => {
|
|
1224
|
+
if (!Object.prototype.hasOwnProperty.call(rawInput, property)) return false;
|
|
1225
|
+
return matchesJSONSchemaValue({
|
|
1226
|
+
value: rawInput[property],
|
|
1227
|
+
schema: properties[property]
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
function isJSONSchemaDiscriminator({ schema }) {
|
|
1232
|
+
return isRecord3(schema) && ("const" in schema || Array.isArray(schema.enum) && schema.enum.length === 1);
|
|
1233
|
+
}
|
|
1234
|
+
function matchesJSONSchemaValue({
|
|
1235
|
+
value,
|
|
1236
|
+
schema
|
|
1237
|
+
}) {
|
|
1238
|
+
if (schema === true) return true;
|
|
1239
|
+
if (schema === false || !isRecord3(schema)) return false;
|
|
1240
|
+
if ("const" in schema && !Object.is(value, schema.const)) return false;
|
|
1241
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => Object.is(value, candidate))) {
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
for (const alternatives of [schema.anyOf, schema.oneOf]) {
|
|
1245
|
+
if (Array.isArray(alternatives) && !alternatives.some(
|
|
1246
|
+
(alternative) => matchesJSONSchemaValue({ value, schema: alternative })
|
|
1247
|
+
)) {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (typeof schema.type !== "string") return true;
|
|
1252
|
+
switch (schema.type) {
|
|
1253
|
+
case "null":
|
|
1254
|
+
return value === null;
|
|
1255
|
+
case "boolean":
|
|
1256
|
+
return typeof value === "boolean";
|
|
1257
|
+
case "integer":
|
|
1258
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
1259
|
+
case "number":
|
|
1260
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1261
|
+
case "string":
|
|
1262
|
+
return typeof value === "string";
|
|
1263
|
+
case "array":
|
|
1264
|
+
return Array.isArray(value);
|
|
1265
|
+
case "object":
|
|
1266
|
+
return isRecord3(value);
|
|
1267
|
+
default:
|
|
1268
|
+
return true;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function mergeToolUpdate({
|
|
1272
|
+
state,
|
|
1273
|
+
update,
|
|
1274
|
+
rawUpdate
|
|
1275
|
+
}) {
|
|
1276
|
+
const parsed = update;
|
|
1277
|
+
for (const property of [
|
|
1278
|
+
"title",
|
|
1279
|
+
"kind",
|
|
1280
|
+
"status",
|
|
1281
|
+
"content",
|
|
1282
|
+
"locations",
|
|
1283
|
+
"rawInput",
|
|
1284
|
+
"rawOutput",
|
|
1285
|
+
"_meta"
|
|
1286
|
+
]) {
|
|
1287
|
+
if (Object.prototype.hasOwnProperty.call(parsed, property) && parsed[property] !== void 0) {
|
|
1288
|
+
state.values[property] = parsed[property];
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
const programmaticName = getStringProperty2({
|
|
1292
|
+
value: rawUpdate,
|
|
1293
|
+
property: "name"
|
|
1294
|
+
});
|
|
1295
|
+
if (programmaticName != null) state.values.name = programmaticName;
|
|
1296
|
+
}
|
|
1297
|
+
function emitFileChanges({
|
|
1298
|
+
state,
|
|
1299
|
+
emit
|
|
1300
|
+
}) {
|
|
1301
|
+
const content = Array.isArray(state.values.content) ? state.values.content : [];
|
|
1302
|
+
for (const item of content) {
|
|
1303
|
+
if (!isRecord3(item) || item.type !== "diff") continue;
|
|
1304
|
+
const path = getStringProperty2({ value: item, property: "path" });
|
|
1305
|
+
const newText = getStringProperty2({ value: item, property: "newText" });
|
|
1306
|
+
if (path == null || newText == null) continue;
|
|
1307
|
+
const oldText = item.oldText;
|
|
1308
|
+
const event = oldText == null ? "create" : "modify";
|
|
1309
|
+
emitFileChange({ state, emit, path, event });
|
|
1310
|
+
}
|
|
1311
|
+
if (state.values.kind !== "edit") return;
|
|
1312
|
+
const locations = Array.isArray(state.values.locations) ? state.values.locations : [];
|
|
1313
|
+
for (const location of locations) {
|
|
1314
|
+
const path = getStringProperty2({ value: location, property: "path" });
|
|
1315
|
+
if (path != null) {
|
|
1316
|
+
emitFileChange({ state, emit, path, event: "modify" });
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function emitFileChange({
|
|
1321
|
+
state,
|
|
1322
|
+
emit,
|
|
1323
|
+
path,
|
|
1324
|
+
event
|
|
1325
|
+
}) {
|
|
1326
|
+
if (state.emittedFilePaths.has(path)) return;
|
|
1327
|
+
state.emittedFilePaths.add(path);
|
|
1328
|
+
emit({
|
|
1329
|
+
type: "file-change",
|
|
1330
|
+
path,
|
|
1331
|
+
event,
|
|
1332
|
+
harnessMetadata: {
|
|
1333
|
+
acp: {
|
|
1334
|
+
toolCallId: state.toolCallId
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
function createDynamicToolName({
|
|
1340
|
+
programmaticName,
|
|
1341
|
+
toolCallId
|
|
1342
|
+
}) {
|
|
1343
|
+
const source = programmaticName ?? `acp_tool_${toolCallId}`;
|
|
1344
|
+
const normalized = source.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 128);
|
|
1345
|
+
if (normalized.length === 0) return "acp_tool";
|
|
1346
|
+
return /^[A-Za-z_]/.test(normalized) ? normalized : `acp_${normalized}`;
|
|
1347
|
+
}
|
|
1348
|
+
function stringifyToolInput({ input }) {
|
|
1349
|
+
return JSON.stringify(toSafeJSONValue({ value: input, fallback: {} }));
|
|
1350
|
+
}
|
|
1351
|
+
function createToolResult({ state }) {
|
|
1352
|
+
const value = state.values.rawOutput != null ? state.values.rawOutput : state.values.content != null ? state.values.content : {};
|
|
1353
|
+
return toSafeJSONValue({ value, fallback: {} }) ?? {};
|
|
1354
|
+
}
|
|
1355
|
+
function toSafeJSONValue({
|
|
1356
|
+
value,
|
|
1357
|
+
fallback,
|
|
1358
|
+
seen = /* @__PURE__ */ new Set()
|
|
1359
|
+
}) {
|
|
1360
|
+
if (value == null) return fallback;
|
|
1361
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
1362
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
1363
|
+
if (typeof value === "bigint") return value.toString();
|
|
1364
|
+
if (typeof value !== "object") return fallback;
|
|
1365
|
+
if (seen.has(value)) return "[Circular]";
|
|
1366
|
+
seen.add(value);
|
|
1367
|
+
if (Array.isArray(value)) {
|
|
1368
|
+
const result2 = value.map(
|
|
1369
|
+
(item) => toSafeJSONValue({ value: item, fallback: null, seen })
|
|
1370
|
+
);
|
|
1371
|
+
seen.delete(value);
|
|
1372
|
+
return result2;
|
|
1373
|
+
}
|
|
1374
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
1375
|
+
try {
|
|
1376
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1377
|
+
result[key] = toSafeJSONValue({
|
|
1378
|
+
value: item,
|
|
1379
|
+
fallback: null,
|
|
1380
|
+
seen
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
} catch {
|
|
1384
|
+
seen.delete(value);
|
|
1385
|
+
return fallback;
|
|
1386
|
+
}
|
|
1387
|
+
seen.delete(value);
|
|
1388
|
+
return result;
|
|
1389
|
+
}
|
|
1390
|
+
function getStringProperty2({
|
|
1391
|
+
value,
|
|
1392
|
+
property
|
|
1393
|
+
}) {
|
|
1394
|
+
if (!isRecord3(value)) return void 0;
|
|
1395
|
+
const propertyValue = value[property];
|
|
1396
|
+
return typeof propertyValue === "string" ? propertyValue : void 0;
|
|
1397
|
+
}
|
|
1398
|
+
function isRecord3(value) {
|
|
1399
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1400
|
+
}
|
|
1401
|
+
function mapACPUsage({
|
|
1402
|
+
usage
|
|
1403
|
+
}) {
|
|
1404
|
+
if (usage == null) return unknownUsage();
|
|
1405
|
+
const raw = mapACPUsageMetadata({ usage });
|
|
1406
|
+
return {
|
|
1407
|
+
inputTokens: {
|
|
1408
|
+
total: usage.inputTokens,
|
|
1409
|
+
noCache: void 0,
|
|
1410
|
+
cacheRead: usage.cachedReadTokens ?? void 0,
|
|
1411
|
+
cacheWrite: usage.cachedWriteTokens ?? void 0
|
|
1412
|
+
},
|
|
1413
|
+
outputTokens: {
|
|
1414
|
+
total: usage.outputTokens,
|
|
1415
|
+
text: void 0,
|
|
1416
|
+
reasoning: usage.thoughtTokens ?? void 0
|
|
1417
|
+
},
|
|
1418
|
+
raw
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
function mapACPUsageMetadata({
|
|
1422
|
+
usage
|
|
1423
|
+
}) {
|
|
1424
|
+
return {
|
|
1425
|
+
totalTokens: usage.totalTokens,
|
|
1426
|
+
inputTokens: usage.inputTokens,
|
|
1427
|
+
outputTokens: usage.outputTokens,
|
|
1428
|
+
...usage.thoughtTokens == null ? {} : { thoughtTokens: usage.thoughtTokens },
|
|
1429
|
+
...usage.cachedReadTokens == null ? {} : { cachedReadTokens: usage.cachedReadTokens },
|
|
1430
|
+
...usage.cachedWriteTokens == null ? {} : { cachedWriteTokens: usage.cachedWriteTokens }
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
function unknownUsage() {
|
|
1434
|
+
return {
|
|
1435
|
+
inputTokens: {
|
|
1436
|
+
total: void 0,
|
|
1437
|
+
noCache: void 0,
|
|
1438
|
+
cacheRead: void 0,
|
|
1439
|
+
cacheWrite: void 0
|
|
1440
|
+
},
|
|
1441
|
+
outputTokens: {
|
|
1442
|
+
total: void 0,
|
|
1443
|
+
text: void 0,
|
|
1444
|
+
reasoning: void 0
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/v1/bridge/host-tool-correlation.ts
|
|
1450
|
+
var CORRELATION_WINDOW_MS = 1e3;
|
|
1451
|
+
var MAX_BUFFERED_UPDATES = 128;
|
|
1452
|
+
function createHostToolCorrelation({
|
|
1453
|
+
emitSemanticUpdate,
|
|
1454
|
+
emitRawUpdate,
|
|
1455
|
+
hostToolServerName,
|
|
1456
|
+
hostTools
|
|
1457
|
+
}) {
|
|
1458
|
+
const invocations = /* @__PURE__ */ new Map();
|
|
1459
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
1460
|
+
const suppressedToolCallIds = /* @__PURE__ */ new Set();
|
|
1461
|
+
const releasedToolCallIds = /* @__PURE__ */ new Set();
|
|
1462
|
+
let nextCandidateOrder = 0;
|
|
1463
|
+
let buffered = [];
|
|
1464
|
+
let flushTimer;
|
|
1465
|
+
const flush = () => {
|
|
1466
|
+
if (flushTimer != null) {
|
|
1467
|
+
clearTimeout(flushTimer);
|
|
1468
|
+
flushTimer = void 0;
|
|
1469
|
+
}
|
|
1470
|
+
if (buffered.length === 0) return;
|
|
1471
|
+
const updates = buffered;
|
|
1472
|
+
buffered = [];
|
|
1473
|
+
for (const update of updates) {
|
|
1474
|
+
if (isToolUpdate(update.message) && suppressedToolCallIds.has(update.message.update.toolCallId)) {
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (isToolUpdate(update.message)) {
|
|
1478
|
+
releasedToolCallIds.add(update.message.update.toolCallId);
|
|
1479
|
+
}
|
|
1480
|
+
emitSemanticUpdate(update);
|
|
1481
|
+
}
|
|
1482
|
+
for (const toolCallId of candidates.keys()) {
|
|
1483
|
+
if (!suppressedToolCallIds.has(toolCallId)) {
|
|
1484
|
+
releasedToolCallIds.add(toolCallId);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
candidates.clear();
|
|
1488
|
+
};
|
|
1489
|
+
const scheduleFlush = () => {
|
|
1490
|
+
if (flushTimer != null) return;
|
|
1491
|
+
flushTimer = setTimeout(flush, CORRELATION_WINDOW_MS);
|
|
1492
|
+
flushTimer.unref?.();
|
|
1493
|
+
};
|
|
1494
|
+
const suppressCandidate = ({
|
|
1495
|
+
candidate,
|
|
1496
|
+
invocation
|
|
1497
|
+
}) => {
|
|
1498
|
+
invocation.toolCallId = candidate.toolCallId;
|
|
1499
|
+
suppressedToolCallIds.add(candidate.toolCallId);
|
|
1500
|
+
candidates.delete(candidate.toolCallId);
|
|
1501
|
+
deleteInvocation({ invocations, token: invocation.token });
|
|
1502
|
+
};
|
|
1503
|
+
const suppressToolCallUpdate = ({ toolCallId }) => {
|
|
1504
|
+
suppressedToolCallIds.add(toolCallId);
|
|
1505
|
+
candidates.delete(toolCallId);
|
|
1506
|
+
if (candidates.size === 0) flush();
|
|
1507
|
+
};
|
|
1508
|
+
const reconcile = () => {
|
|
1509
|
+
const orderedInvocations = [...invocations.values()].sort(
|
|
1510
|
+
(left, right) => left.order - right.order
|
|
1511
|
+
);
|
|
1512
|
+
const orderedCandidates = [...candidates.values()].sort(
|
|
1513
|
+
(left, right) => left.order - right.order
|
|
1514
|
+
);
|
|
1515
|
+
for (const invocation of orderedInvocations) {
|
|
1516
|
+
if (invocation.toolCallId != null) continue;
|
|
1517
|
+
const candidate = orderedCandidates.find(
|
|
1518
|
+
(item) => candidates.has(item.toolCallId) && hasPortableEvidence({ candidate: item, invocation })
|
|
1519
|
+
);
|
|
1520
|
+
if (candidate != null) suppressCandidate({ candidate, invocation });
|
|
1521
|
+
}
|
|
1522
|
+
if (candidates.size === 0) flush();
|
|
1523
|
+
};
|
|
1524
|
+
return {
|
|
1525
|
+
update: ({ message, rawUpdate }) => {
|
|
1526
|
+
const preservedUpdate = rawUpdate === void 0 ? message.update : rawUpdate;
|
|
1527
|
+
emitRawUpdate({ rawUpdate: preservedUpdate });
|
|
1528
|
+
if (!isToolUpdate(message)) {
|
|
1529
|
+
if (buffered.length === 0) {
|
|
1530
|
+
emitSemanticUpdate({ message, rawUpdate: preservedUpdate });
|
|
1531
|
+
} else {
|
|
1532
|
+
buffered.push({ message, rawUpdate: preservedUpdate });
|
|
1533
|
+
if (buffered.length >= MAX_BUFFERED_UPDATES) flush();
|
|
1534
|
+
}
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
const toolCallId = message.update.toolCallId;
|
|
1538
|
+
if (suppressedToolCallIds.has(toolCallId)) {
|
|
1539
|
+
if (isTerminalToolUpdate(message)) {
|
|
1540
|
+
suppressedToolCallIds.delete(toolCallId);
|
|
1541
|
+
removeInvocationForToolCall({ invocations, toolCallId });
|
|
1542
|
+
}
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
if (releasedToolCallIds.has(toolCallId)) {
|
|
1546
|
+
emitSemanticUpdate({ message, rawUpdate: preservedUpdate });
|
|
1547
|
+
if (isTerminalToolUpdate(message))
|
|
1548
|
+
releasedToolCallIds.delete(toolCallId);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
buffered.push({ message, rawUpdate: preservedUpdate });
|
|
1552
|
+
const candidate = candidates.get(toolCallId) ?? {
|
|
1553
|
+
toolCallId,
|
|
1554
|
+
order: ++nextCandidateOrder,
|
|
1555
|
+
evidence: []
|
|
1556
|
+
};
|
|
1557
|
+
candidate.evidence.push(message.update, preservedUpdate);
|
|
1558
|
+
candidates.set(toolCallId, candidate);
|
|
1559
|
+
const tokenMatch = findTokenInvocation({
|
|
1560
|
+
value: preservedUpdate,
|
|
1561
|
+
invocations
|
|
1562
|
+
});
|
|
1563
|
+
if (tokenMatch != null) {
|
|
1564
|
+
suppressCandidate({ candidate, invocation: tokenMatch });
|
|
1565
|
+
}
|
|
1566
|
+
reconcile();
|
|
1567
|
+
if (buffered.length >= MAX_BUFFERED_UPDATES) flush();
|
|
1568
|
+
else if (buffered.length > 0) scheduleFlush();
|
|
1569
|
+
},
|
|
1570
|
+
registerInvocation: ({ token, serverName, toolName, input, order }) => {
|
|
1571
|
+
const invocation = {
|
|
1572
|
+
token,
|
|
1573
|
+
serverName,
|
|
1574
|
+
toolName,
|
|
1575
|
+
inputFingerprint: canonicalFingerprint({ value: input }),
|
|
1576
|
+
order
|
|
1577
|
+
};
|
|
1578
|
+
invocations.set(token, invocation);
|
|
1579
|
+
invocation.expiryTimer = setTimeout(
|
|
1580
|
+
() => deleteInvocation({ invocations, token }),
|
|
1581
|
+
CORRELATION_WINDOW_MS
|
|
1582
|
+
);
|
|
1583
|
+
invocation.expiryTimer.unref?.();
|
|
1584
|
+
const tokenCandidate = [...candidates.values()].find(
|
|
1585
|
+
(candidate) => candidate.evidence.some(
|
|
1586
|
+
(value) => containsExactValue({ value, target: token })
|
|
1587
|
+
)
|
|
1588
|
+
);
|
|
1589
|
+
if (tokenCandidate != null) {
|
|
1590
|
+
suppressCandidate({ candidate: tokenCandidate, invocation });
|
|
1591
|
+
}
|
|
1592
|
+
reconcile();
|
|
1593
|
+
},
|
|
1594
|
+
claimHostToolPermission: ({ toolCall }) => {
|
|
1595
|
+
const candidate = candidates.get(toolCall.toolCallId);
|
|
1596
|
+
const matches = hostTools.filter(({ name: name2 }) => {
|
|
1597
|
+
const permission = resolvePermissionHostTool({
|
|
1598
|
+
toolCall,
|
|
1599
|
+
serverName: hostToolServerName,
|
|
1600
|
+
toolName: name2
|
|
1601
|
+
});
|
|
1602
|
+
if (permission == null) return false;
|
|
1603
|
+
if (candidate == null) return permission.hasRequestIdentity;
|
|
1604
|
+
return hasPortableEvidence({
|
|
1605
|
+
candidate,
|
|
1606
|
+
invocation: {
|
|
1607
|
+
token: "",
|
|
1608
|
+
serverName: hostToolServerName,
|
|
1609
|
+
toolName: name2,
|
|
1610
|
+
inputFingerprint: canonicalFingerprint({
|
|
1611
|
+
value: permission.input
|
|
1612
|
+
}),
|
|
1613
|
+
order: 0
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
});
|
|
1617
|
+
if (matches.length !== 1) return false;
|
|
1618
|
+
suppressToolCallUpdate({ toolCallId: toolCall.toolCallId });
|
|
1619
|
+
return true;
|
|
1620
|
+
},
|
|
1621
|
+
flush,
|
|
1622
|
+
removeInvocation: ({ token }) => {
|
|
1623
|
+
deleteInvocation({ invocations, token });
|
|
1624
|
+
},
|
|
1625
|
+
close: () => {
|
|
1626
|
+
flush();
|
|
1627
|
+
for (const invocation of invocations.values()) {
|
|
1628
|
+
if (invocation.expiryTimer != null) {
|
|
1629
|
+
clearTimeout(invocation.expiryTimer);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
invocations.clear();
|
|
1633
|
+
candidates.clear();
|
|
1634
|
+
suppressedToolCallIds.clear();
|
|
1635
|
+
releasedToolCallIds.clear();
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
function hasPortableEvidence({
|
|
1640
|
+
candidate,
|
|
1641
|
+
invocation
|
|
1642
|
+
}) {
|
|
1643
|
+
let hasServerIdentity = false;
|
|
1644
|
+
let hasToolName = false;
|
|
1645
|
+
let hasInput = false;
|
|
1646
|
+
for (const evidence of candidate.evidence) {
|
|
1647
|
+
const metadata = getProperty({ value: evidence, property: "_meta" });
|
|
1648
|
+
const rawInput = getProperty({ value: evidence, property: "rawInput" });
|
|
1649
|
+
const programmaticName = getProperty({
|
|
1650
|
+
value: evidence,
|
|
1651
|
+
property: "name"
|
|
1652
|
+
});
|
|
1653
|
+
const deferredToolName = getProperty({
|
|
1654
|
+
value: rawInput,
|
|
1655
|
+
property: "tool_name"
|
|
1656
|
+
});
|
|
1657
|
+
const deferredToolInput = getProperty({
|
|
1658
|
+
value: rawInput,
|
|
1659
|
+
property: "tool_input"
|
|
1660
|
+
});
|
|
1661
|
+
if (containsCombinedIdentity({
|
|
1662
|
+
value: [metadata, programmaticName],
|
|
1663
|
+
serverName: invocation.serverName,
|
|
1664
|
+
toolName: invocation.toolName
|
|
1665
|
+
})) {
|
|
1666
|
+
hasServerIdentity = true;
|
|
1667
|
+
hasToolName = true;
|
|
1668
|
+
}
|
|
1669
|
+
if (typeof deferredToolName === "string" && isRecord4(deferredToolInput) && hasDelimitedPair({
|
|
1670
|
+
value: deferredToolName,
|
|
1671
|
+
serverName: invocation.serverName,
|
|
1672
|
+
toolName: invocation.toolName
|
|
1673
|
+
})) {
|
|
1674
|
+
hasServerIdentity = true;
|
|
1675
|
+
hasToolName = true;
|
|
1676
|
+
if (canonicalFingerprint({ value: deferredToolInput }) === invocation.inputFingerprint) {
|
|
1677
|
+
hasInput = true;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (containsExactValue({
|
|
1681
|
+
value: [metadata, rawInput],
|
|
1682
|
+
target: invocation.serverName
|
|
1683
|
+
})) {
|
|
1684
|
+
hasServerIdentity = true;
|
|
1685
|
+
}
|
|
1686
|
+
if (programmaticName === invocation.toolName || containsExactValue({
|
|
1687
|
+
value: [metadata, rawInput],
|
|
1688
|
+
target: invocation.toolName
|
|
1689
|
+
})) {
|
|
1690
|
+
hasToolName = true;
|
|
1691
|
+
}
|
|
1692
|
+
if (containsFingerprint({
|
|
1693
|
+
value: [metadata, rawInput],
|
|
1694
|
+
target: invocation.inputFingerprint
|
|
1695
|
+
})) {
|
|
1696
|
+
hasInput = true;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return hasServerIdentity && hasToolName && hasInput;
|
|
1700
|
+
}
|
|
1701
|
+
function resolvePermissionHostTool({
|
|
1702
|
+
toolCall,
|
|
1703
|
+
serverName,
|
|
1704
|
+
toolName
|
|
1705
|
+
}) {
|
|
1706
|
+
if (!isRecord4(toolCall.rawInput)) return void 0;
|
|
1707
|
+
const deferredToolName = getProperty({
|
|
1708
|
+
value: toolCall.rawInput,
|
|
1709
|
+
property: "tool_name"
|
|
1710
|
+
});
|
|
1711
|
+
const deferredToolInput = getProperty({
|
|
1712
|
+
value: toolCall.rawInput,
|
|
1713
|
+
property: "tool_input"
|
|
1714
|
+
});
|
|
1715
|
+
if (hasOwnProperty({ value: toolCall.rawInput, property: "tool_name" }) && hasOwnProperty({ value: toolCall.rawInput, property: "tool_input" })) {
|
|
1716
|
+
if (typeof deferredToolName !== "string" || !isRecord4(deferredToolInput) || !hasDelimitedPair({
|
|
1717
|
+
value: deferredToolName,
|
|
1718
|
+
serverName,
|
|
1719
|
+
toolName
|
|
1720
|
+
})) {
|
|
1721
|
+
return void 0;
|
|
1722
|
+
}
|
|
1723
|
+
return {
|
|
1724
|
+
input: deferredToolInput,
|
|
1725
|
+
hasRequestIdentity: true
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
return {
|
|
1729
|
+
input: toolCall.rawInput,
|
|
1730
|
+
hasRequestIdentity: containsCombinedIdentity({
|
|
1731
|
+
value: [
|
|
1732
|
+
getProperty({ value: toolCall, property: "title" }),
|
|
1733
|
+
getProperty({ value: toolCall, property: "name" }),
|
|
1734
|
+
getProperty({ value: toolCall, property: "_meta" })
|
|
1735
|
+
],
|
|
1736
|
+
serverName,
|
|
1737
|
+
toolName
|
|
1738
|
+
})
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function containsCombinedIdentity({
|
|
1742
|
+
value,
|
|
1743
|
+
serverName,
|
|
1744
|
+
toolName
|
|
1745
|
+
}) {
|
|
1746
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1747
|
+
const visit = (candidate) => {
|
|
1748
|
+
if (typeof candidate === "string") {
|
|
1749
|
+
return hasDelimitedPair({ value: candidate, serverName, toolName });
|
|
1750
|
+
}
|
|
1751
|
+
if (candidate == null || typeof candidate !== "object") return false;
|
|
1752
|
+
if (seen.has(candidate)) return false;
|
|
1753
|
+
seen.add(candidate);
|
|
1754
|
+
const values = Array.isArray(candidate) ? candidate : Object.values(candidate);
|
|
1755
|
+
for (const item of values) {
|
|
1756
|
+
if (visit(item)) return true;
|
|
1757
|
+
}
|
|
1758
|
+
return false;
|
|
1759
|
+
};
|
|
1760
|
+
return visit(value);
|
|
1761
|
+
}
|
|
1762
|
+
function hasDelimitedPair({
|
|
1763
|
+
value,
|
|
1764
|
+
serverName,
|
|
1765
|
+
toolName
|
|
1766
|
+
}) {
|
|
1767
|
+
if (serverName.length === 0 || toolName.length === 0) return false;
|
|
1768
|
+
let serverIndex = value.indexOf(serverName);
|
|
1769
|
+
while (serverIndex !== -1) {
|
|
1770
|
+
const beforeServer = serverIndex === 0 ? void 0 : value[serverIndex - 1];
|
|
1771
|
+
const serverHasLeadingBoundary = beforeServer == null || value.slice(0, serverIndex).endsWith("__") || !/[A-Za-z0-9_-]/.test(beforeServer);
|
|
1772
|
+
const delimiterStart = serverIndex + serverName.length;
|
|
1773
|
+
let toolIndex = delimiterStart;
|
|
1774
|
+
while (toolIndex < value.length && !/[A-Za-z0-9-]/.test(value[toolIndex])) {
|
|
1775
|
+
toolIndex += 1;
|
|
1776
|
+
}
|
|
1777
|
+
const toolEnd = toolIndex + toolName.length;
|
|
1778
|
+
const afterTool = toolEnd === value.length ? void 0 : value[toolEnd];
|
|
1779
|
+
const toolHasTrailingBoundary = afterTool == null || value.startsWith("__", toolEnd) || !/[A-Za-z0-9_-]/.test(afterTool);
|
|
1780
|
+
if (serverHasLeadingBoundary && toolIndex > delimiterStart && value.startsWith(toolName, toolIndex) && toolHasTrailingBoundary) {
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1783
|
+
serverIndex = value.indexOf(serverName, serverIndex + 1);
|
|
1784
|
+
}
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
function findTokenInvocation({
|
|
1788
|
+
value,
|
|
1789
|
+
invocations
|
|
1790
|
+
}) {
|
|
1791
|
+
for (const invocation of invocations.values()) {
|
|
1792
|
+
if (containsExactValue({ value, target: invocation.token })) {
|
|
1793
|
+
return invocation;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return void 0;
|
|
1797
|
+
}
|
|
1798
|
+
function removeInvocationForToolCall({
|
|
1799
|
+
invocations,
|
|
1800
|
+
toolCallId
|
|
1801
|
+
}) {
|
|
1802
|
+
for (const [token, invocation] of invocations) {
|
|
1803
|
+
if (invocation.toolCallId === toolCallId) {
|
|
1804
|
+
deleteInvocation({ invocations, token });
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
function deleteInvocation({
|
|
1809
|
+
invocations,
|
|
1810
|
+
token
|
|
1811
|
+
}) {
|
|
1812
|
+
const invocation = invocations.get(token);
|
|
1813
|
+
if (invocation?.expiryTimer != null) clearTimeout(invocation.expiryTimer);
|
|
1814
|
+
invocations.delete(token);
|
|
1815
|
+
}
|
|
1816
|
+
function isToolUpdate(message) {
|
|
1817
|
+
return message.update.sessionUpdate === "tool_call" || message.update.sessionUpdate === "tool_call_update";
|
|
1818
|
+
}
|
|
1819
|
+
function containsExactValue({
|
|
1820
|
+
value,
|
|
1821
|
+
target
|
|
1822
|
+
}) {
|
|
1823
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1824
|
+
const visit = (candidate) => {
|
|
1825
|
+
if (candidate === target) return true;
|
|
1826
|
+
if (candidate == null || typeof candidate !== "object") return false;
|
|
1827
|
+
if (seen.has(candidate)) return false;
|
|
1828
|
+
seen.add(candidate);
|
|
1829
|
+
const values = Array.isArray(candidate) ? candidate : Object.values(candidate);
|
|
1830
|
+
for (const item of values) {
|
|
1831
|
+
if (visit(item)) return true;
|
|
1832
|
+
}
|
|
1833
|
+
return false;
|
|
1834
|
+
};
|
|
1835
|
+
return visit(value);
|
|
1836
|
+
}
|
|
1837
|
+
function containsFingerprint({
|
|
1838
|
+
value,
|
|
1839
|
+
target
|
|
1840
|
+
}) {
|
|
1841
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1842
|
+
const visit = (candidate) => {
|
|
1843
|
+
if (candidate != null && typeof candidate === "object") {
|
|
1844
|
+
if (canonicalFingerprint({ value: candidate }) === target) return true;
|
|
1845
|
+
if (seen.has(candidate)) return false;
|
|
1846
|
+
seen.add(candidate);
|
|
1847
|
+
const values = Array.isArray(candidate) ? candidate : Object.values(candidate);
|
|
1848
|
+
for (const item of values) {
|
|
1849
|
+
if (visit(item)) return true;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return false;
|
|
1853
|
+
};
|
|
1854
|
+
return visit(value);
|
|
1855
|
+
}
|
|
1856
|
+
function canonicalFingerprint({ value }) {
|
|
1857
|
+
return JSON.stringify(canonicalizeJSON({ value }));
|
|
1858
|
+
}
|
|
1859
|
+
function canonicalizeJSON({ value }) {
|
|
1860
|
+
if (Array.isArray(value)) {
|
|
1861
|
+
return value.map((item) => canonicalizeJSON({ value: item }));
|
|
1862
|
+
}
|
|
1863
|
+
if (value == null || typeof value !== "object") return value;
|
|
1864
|
+
return Object.fromEntries(
|
|
1865
|
+
Object.keys(value).sort().filter((key) => value[key] !== void 0).map((key) => [
|
|
1866
|
+
key,
|
|
1867
|
+
canonicalizeJSON({
|
|
1868
|
+
value: value[key]
|
|
1869
|
+
})
|
|
1870
|
+
])
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
function getProperty({
|
|
1874
|
+
value,
|
|
1875
|
+
property
|
|
1876
|
+
}) {
|
|
1877
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
1878
|
+
return void 0;
|
|
1879
|
+
}
|
|
1880
|
+
return Reflect.get(value, property);
|
|
1881
|
+
}
|
|
1882
|
+
function hasOwnProperty({
|
|
1883
|
+
value,
|
|
1884
|
+
property
|
|
1885
|
+
}) {
|
|
1886
|
+
return Object.prototype.hasOwnProperty.call(value, property);
|
|
1887
|
+
}
|
|
1888
|
+
function isRecord4(value) {
|
|
1889
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1890
|
+
}
|
|
1891
|
+
function isTerminalToolUpdate(message) {
|
|
1892
|
+
return isToolUpdate(message) && (message.update.status === "completed" || message.update.status === "failed");
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// src/v1/bridge/create-emit-stream-event.ts
|
|
1896
|
+
function createEmitStreamEvent({
|
|
1897
|
+
emit,
|
|
1898
|
+
builtinTools,
|
|
1899
|
+
hostToolServerName,
|
|
1900
|
+
hostTools
|
|
1901
|
+
}) {
|
|
1902
|
+
const translator = createACPStreamTranslator({ emit, builtinTools });
|
|
1903
|
+
const correlation = createHostToolCorrelation({
|
|
1904
|
+
emitSemanticUpdate: ({ message, rawUpdate }) => {
|
|
1905
|
+
translator.update({
|
|
1906
|
+
update: message.update,
|
|
1907
|
+
rawUpdate,
|
|
1908
|
+
preserveRaw: false
|
|
1909
|
+
});
|
|
1910
|
+
},
|
|
1911
|
+
emitRawUpdate: ({ rawUpdate }) => {
|
|
1912
|
+
translator.raw({ rawValue: rawUpdate });
|
|
1913
|
+
},
|
|
1914
|
+
hostToolServerName,
|
|
1915
|
+
hostTools
|
|
1916
|
+
});
|
|
1917
|
+
return {
|
|
1918
|
+
message: ({ message, rawUpdate }) => {
|
|
1919
|
+
if (message.kind === "stop") {
|
|
1920
|
+
correlation.close();
|
|
1921
|
+
translator.finish(message.response);
|
|
1922
|
+
return true;
|
|
1923
|
+
}
|
|
1924
|
+
correlation.update({ message, rawUpdate });
|
|
1925
|
+
return false;
|
|
1926
|
+
},
|
|
1927
|
+
raw: translator.raw,
|
|
1928
|
+
close: () => {
|
|
1929
|
+
correlation.close();
|
|
1930
|
+
translator.close();
|
|
1931
|
+
},
|
|
1932
|
+
permissionToolCall: ({ toolCall }) => {
|
|
1933
|
+
translator.permissionToolCall({
|
|
1934
|
+
toolCall: {
|
|
1935
|
+
sessionUpdate: "tool_call_update",
|
|
1936
|
+
...toolCall
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
},
|
|
1940
|
+
claimHostToolPermission: correlation.claimHostToolPermission,
|
|
1941
|
+
hostToolCall: translator.hostToolCall,
|
|
1942
|
+
hostToolResult: translator.hostToolResult,
|
|
1943
|
+
registerHostToolCorrelationInvocation: correlation.registerInvocation,
|
|
1944
|
+
removeHostToolCorrelationInvocation: correlation.removeInvocation
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// src/v1/bridge/host-tool-relay.ts
|
|
1949
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
1950
|
+
import { createServer } from "http";
|
|
1951
|
+
async function startHostToolRelay({
|
|
1952
|
+
tools,
|
|
1953
|
+
serverName
|
|
1954
|
+
}) {
|
|
1955
|
+
const state = {
|
|
1956
|
+
tools: [...tools],
|
|
1957
|
+
fingerprint: catalogFingerprint({ tools }),
|
|
1958
|
+
revision: 1,
|
|
1959
|
+
servedRevision: 0,
|
|
1960
|
+
closed: false,
|
|
1961
|
+
changeWaiters: /* @__PURE__ */ new Set(),
|
|
1962
|
+
refreshWaiters: /* @__PURE__ */ new Set()
|
|
1963
|
+
};
|
|
1964
|
+
const credential = randomBytes(32).toString("hex");
|
|
1965
|
+
let activeTurn;
|
|
1966
|
+
let invocationOrder = 0;
|
|
1967
|
+
let closePromise;
|
|
1968
|
+
const server = createServer(async (request, response) => {
|
|
1969
|
+
try {
|
|
1970
|
+
const result = await handleRequest({
|
|
1971
|
+
request,
|
|
1972
|
+
credential,
|
|
1973
|
+
state,
|
|
1974
|
+
serverName,
|
|
1975
|
+
turn: activeTurn,
|
|
1976
|
+
nextInvocationOrder: () => ++invocationOrder
|
|
1977
|
+
});
|
|
1978
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
1979
|
+
response.end(JSON.stringify(result));
|
|
1980
|
+
} catch (error) {
|
|
1981
|
+
const status = error instanceof RelayRequestError ? error.status : 500;
|
|
1982
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
1983
|
+
response.end(
|
|
1984
|
+
JSON.stringify({
|
|
1985
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1986
|
+
})
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
await listen({ server });
|
|
1991
|
+
const address = server.address();
|
|
1992
|
+
return {
|
|
1993
|
+
url: `http://127.0.0.1:${address.port}/invoke`,
|
|
1994
|
+
credential,
|
|
1995
|
+
bindTurn: ({ turn }) => {
|
|
1996
|
+
if (activeTurn != null && activeTurn !== turn) {
|
|
1997
|
+
throw new Error("A host tool relay turn is already active.");
|
|
1998
|
+
}
|
|
1999
|
+
activeTurn = turn;
|
|
2000
|
+
},
|
|
2001
|
+
unbindTurn: ({ turn }) => {
|
|
2002
|
+
if (activeTurn === turn) activeTurn = void 0;
|
|
2003
|
+
},
|
|
2004
|
+
updateCatalog: ({ tools: nextTools }) => {
|
|
2005
|
+
const fingerprint = catalogFingerprint({ tools: nextTools });
|
|
2006
|
+
if (fingerprint === state.fingerprint) {
|
|
2007
|
+
return { changed: false, revision: state.revision };
|
|
2008
|
+
}
|
|
2009
|
+
state.tools = [...nextTools];
|
|
2010
|
+
state.fingerprint = fingerprint;
|
|
2011
|
+
state.revision += 1;
|
|
2012
|
+
resolveCatalogChanges({ state });
|
|
2013
|
+
return { changed: true, revision: state.revision };
|
|
2014
|
+
},
|
|
2015
|
+
waitForCatalogRefresh: ({ revision, timeoutMs }) => waitForCatalogRefresh({ state, revision, timeoutMs }),
|
|
2016
|
+
close: () => {
|
|
2017
|
+
if (closePromise != null) return closePromise;
|
|
2018
|
+
state.closed = true;
|
|
2019
|
+
resolveCatalogChanges({ state });
|
|
2020
|
+
resolveRefreshWaiters({ state, closing: true });
|
|
2021
|
+
closePromise = closeServer({ server });
|
|
2022
|
+
return closePromise;
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
async function handleRequest({
|
|
2027
|
+
request,
|
|
2028
|
+
credential,
|
|
2029
|
+
state,
|
|
2030
|
+
serverName,
|
|
2031
|
+
turn,
|
|
2032
|
+
nextInvocationOrder
|
|
2033
|
+
}) {
|
|
2034
|
+
if (request.method !== "POST") {
|
|
2035
|
+
throw new RelayRequestError({
|
|
2036
|
+
status: 404,
|
|
2037
|
+
message: "Unknown host tool relay endpoint."
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
if (!credentialsMatch({
|
|
2041
|
+
expected: credential,
|
|
2042
|
+
actual: request.headers.authorization
|
|
2043
|
+
})) {
|
|
2044
|
+
throw new RelayRequestError({
|
|
2045
|
+
status: 401,
|
|
2046
|
+
message: "Invalid host tool relay credential."
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
const body = await readJSONBody({ request });
|
|
2050
|
+
if (request.url === "/catalog/next") {
|
|
2051
|
+
return handleCatalogNext({ body, state });
|
|
2052
|
+
}
|
|
2053
|
+
if (request.url === "/catalog/seen") {
|
|
2054
|
+
return handleCatalogSeen({ body, state });
|
|
2055
|
+
}
|
|
2056
|
+
if (request.url === "/invoke") {
|
|
2057
|
+
return handleInvocation({
|
|
2058
|
+
body,
|
|
2059
|
+
state,
|
|
2060
|
+
serverName,
|
|
2061
|
+
turn,
|
|
2062
|
+
nextInvocationOrder
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
throw new RelayRequestError({
|
|
2066
|
+
status: 404,
|
|
2067
|
+
message: "Unknown host tool relay endpoint."
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
async function handleCatalogNext({
|
|
2071
|
+
body,
|
|
2072
|
+
state
|
|
2073
|
+
}) {
|
|
2074
|
+
if (!isRecord5(body) || !Number.isSafeInteger(body.afterRevision) || body.afterRevision < 0) {
|
|
2075
|
+
throw new RelayRequestError({
|
|
2076
|
+
status: 400,
|
|
2077
|
+
message: "Invalid host tool catalog poll request."
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
const afterRevision = body.afterRevision;
|
|
2081
|
+
if (!state.closed && afterRevision >= state.revision) {
|
|
2082
|
+
await waitForCatalogChange({ state });
|
|
2083
|
+
}
|
|
2084
|
+
if (state.closed) {
|
|
2085
|
+
return { closed: true, revision: state.revision };
|
|
2086
|
+
}
|
|
2087
|
+
return afterRevision < state.revision ? { revision: state.revision, tools: state.tools } : { revision: state.revision };
|
|
2088
|
+
}
|
|
2089
|
+
function handleCatalogSeen({
|
|
2090
|
+
body,
|
|
2091
|
+
state
|
|
2092
|
+
}) {
|
|
2093
|
+
if (!isRecord5(body) || !Number.isSafeInteger(body.revision) || body.revision < 1 || body.revision > state.revision) {
|
|
2094
|
+
throw new RelayRequestError({
|
|
2095
|
+
status: 400,
|
|
2096
|
+
message: "Invalid host tool catalog acknowledgment."
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
state.servedRevision = Math.max(
|
|
2100
|
+
state.servedRevision,
|
|
2101
|
+
body.revision
|
|
2102
|
+
);
|
|
2103
|
+
resolveRefreshWaiters({ state, closing: false });
|
|
2104
|
+
return { acknowledged: true };
|
|
2105
|
+
}
|
|
2106
|
+
async function handleInvocation({
|
|
2107
|
+
body,
|
|
2108
|
+
state,
|
|
2109
|
+
serverName,
|
|
2110
|
+
turn,
|
|
2111
|
+
nextInvocationOrder
|
|
2112
|
+
}) {
|
|
2113
|
+
if (turn == null) {
|
|
2114
|
+
throw new RelayRequestError({
|
|
2115
|
+
status: 409,
|
|
2116
|
+
message: "No ACP prompt turn is active."
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
if (!isRecord5(body) || typeof body.requestId !== "string" || typeof body.toolName !== "string" || !isRecord5(body.input) || !Number.isSafeInteger(body.catalogRevision)) {
|
|
2120
|
+
throw new RelayRequestError({
|
|
2121
|
+
status: 400,
|
|
2122
|
+
message: "Invalid host tool relay request."
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
if (body.catalogRevision !== state.revision) {
|
|
2126
|
+
throw new RelayRequestError({
|
|
2127
|
+
status: 409,
|
|
2128
|
+
message: `Host tool ${body.toolName} was invoked from stale catalog revision ${body.catalogRevision}; the active revision is ${state.revision}.`
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
const tool = state.tools.find((item) => item.name === body.toolName);
|
|
2132
|
+
if (tool == null) {
|
|
2133
|
+
throw new RelayRequestError({
|
|
2134
|
+
status: 404,
|
|
2135
|
+
message: `Host tool ${body.toolName} is not active in catalog revision ${state.revision}.`
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
const correlationToken = randomBytes(32).toString("hex");
|
|
2139
|
+
turn.registerCorrelationInvocation({
|
|
2140
|
+
token: correlationToken,
|
|
2141
|
+
serverName,
|
|
2142
|
+
toolName: tool.name,
|
|
2143
|
+
input: body.input,
|
|
2144
|
+
order: nextInvocationOrder()
|
|
2145
|
+
});
|
|
2146
|
+
turn.emitToolCall({
|
|
2147
|
+
toolCallId: body.requestId,
|
|
2148
|
+
toolName: tool.name,
|
|
2149
|
+
input: body.input
|
|
2150
|
+
});
|
|
2151
|
+
let result;
|
|
2152
|
+
try {
|
|
2153
|
+
result = await turn.requestToolResult(body.requestId);
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
turn.removeCorrelationInvocation({ token: correlationToken });
|
|
2156
|
+
throw error;
|
|
2157
|
+
}
|
|
2158
|
+
turn.emitToolResult({
|
|
2159
|
+
toolCallId: body.requestId,
|
|
2160
|
+
toolName: tool.name,
|
|
2161
|
+
output: result.output,
|
|
2162
|
+
...result.isError ? { isError: true } : {}
|
|
2163
|
+
});
|
|
2164
|
+
return {
|
|
2165
|
+
output: result.output,
|
|
2166
|
+
...result.isError ? { isError: true } : {},
|
|
2167
|
+
correlationToken
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
function waitForCatalogChange({
|
|
2171
|
+
state
|
|
2172
|
+
}) {
|
|
2173
|
+
let changeWaiter;
|
|
2174
|
+
const change = new Promise((resolve) => {
|
|
2175
|
+
changeWaiter = resolve;
|
|
2176
|
+
state.changeWaiters.add(changeWaiter);
|
|
2177
|
+
});
|
|
2178
|
+
let timer;
|
|
2179
|
+
const timeout = new Promise((resolve) => {
|
|
2180
|
+
timer = setTimeout(resolve, 2e4);
|
|
2181
|
+
timer.unref?.();
|
|
2182
|
+
});
|
|
2183
|
+
return Promise.race([change, timeout]).finally(() => {
|
|
2184
|
+
state.changeWaiters.delete(changeWaiter);
|
|
2185
|
+
if (timer != null) clearTimeout(timer);
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
function resolveCatalogChanges({ state }) {
|
|
2189
|
+
for (const resolve of [...state.changeWaiters]) resolve();
|
|
2190
|
+
}
|
|
2191
|
+
function waitForCatalogRefresh({
|
|
2192
|
+
state,
|
|
2193
|
+
revision,
|
|
2194
|
+
timeoutMs
|
|
2195
|
+
}) {
|
|
2196
|
+
if (state.servedRevision >= revision) return Promise.resolve(true);
|
|
2197
|
+
if (state.closed) return Promise.resolve(false);
|
|
2198
|
+
let waiter;
|
|
2199
|
+
const refresh = new Promise((resolve) => {
|
|
2200
|
+
waiter = {
|
|
2201
|
+
revision,
|
|
2202
|
+
resolve
|
|
2203
|
+
};
|
|
2204
|
+
state.refreshWaiters.add(waiter);
|
|
2205
|
+
});
|
|
2206
|
+
let timer;
|
|
2207
|
+
const timeout = new Promise((resolve) => {
|
|
2208
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
2209
|
+
timer.unref?.();
|
|
2210
|
+
});
|
|
2211
|
+
return Promise.race([refresh, timeout]).finally(() => {
|
|
2212
|
+
state.refreshWaiters.delete(waiter);
|
|
2213
|
+
if (timer != null) clearTimeout(timer);
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
function resolveRefreshWaiters({
|
|
2217
|
+
state,
|
|
2218
|
+
closing
|
|
2219
|
+
}) {
|
|
2220
|
+
for (const waiter of [...state.refreshWaiters]) {
|
|
2221
|
+
if (closing || state.servedRevision >= waiter.revision) {
|
|
2222
|
+
waiter.resolve(!closing);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
function catalogFingerprint({
|
|
2227
|
+
tools
|
|
2228
|
+
}) {
|
|
2229
|
+
return JSON.stringify(canonicalizeJSON2({ value: tools }));
|
|
2230
|
+
}
|
|
2231
|
+
function canonicalizeJSON2({ value }) {
|
|
2232
|
+
if (Array.isArray(value)) {
|
|
2233
|
+
return value.map((item) => canonicalizeJSON2({ value: item }));
|
|
2234
|
+
}
|
|
2235
|
+
if (!isRecord5(value)) return value;
|
|
2236
|
+
return Object.fromEntries(
|
|
2237
|
+
Object.keys(value).sort().filter((key) => value[key] !== void 0).map((key) => [key, canonicalizeJSON2({ value: value[key] })])
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
async function readJSONBody({
|
|
2241
|
+
request
|
|
2242
|
+
}) {
|
|
2243
|
+
const chunks = [];
|
|
2244
|
+
let size = 0;
|
|
2245
|
+
for await (const chunk of request) {
|
|
2246
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2247
|
+
size += buffer.length;
|
|
2248
|
+
if (size > 16 * 1024 * 1024) {
|
|
2249
|
+
throw new RelayRequestError({
|
|
2250
|
+
status: 413,
|
|
2251
|
+
message: "Host tool relay request is too large."
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
chunks.push(buffer);
|
|
2255
|
+
}
|
|
2256
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
2257
|
+
try {
|
|
2258
|
+
return await new Response(text, {
|
|
2259
|
+
headers: { "content-type": "application/json" }
|
|
2260
|
+
}).json();
|
|
2261
|
+
} catch {
|
|
2262
|
+
throw new RelayRequestError({
|
|
2263
|
+
status: 400,
|
|
2264
|
+
message: "Host tool relay request is not valid JSON."
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function credentialsMatch({
|
|
2269
|
+
expected,
|
|
2270
|
+
actual
|
|
2271
|
+
}) {
|
|
2272
|
+
const expectedValue = Buffer.from(`Bearer ${expected}`);
|
|
2273
|
+
const actualValue = Buffer.from(actual ?? "");
|
|
2274
|
+
return expectedValue.length === actualValue.length && timingSafeEqual(expectedValue, actualValue);
|
|
2275
|
+
}
|
|
2276
|
+
function listen({ server }) {
|
|
2277
|
+
return new Promise((resolve, reject) => {
|
|
2278
|
+
server.once("error", reject);
|
|
2279
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2280
|
+
server.off("error", reject);
|
|
2281
|
+
resolve();
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
function closeServer({ server }) {
|
|
2286
|
+
return new Promise((resolve, reject) => {
|
|
2287
|
+
server.close((error) => {
|
|
2288
|
+
if (error == null) resolve();
|
|
2289
|
+
else reject(error);
|
|
2290
|
+
});
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
function isRecord5(value) {
|
|
2294
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2295
|
+
}
|
|
2296
|
+
var RelayRequestError = class extends Error {
|
|
2297
|
+
constructor({ status, message }) {
|
|
2298
|
+
super(message);
|
|
2299
|
+
this.name = "HostToolRelayRequestError";
|
|
2300
|
+
this.status = status;
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
|
|
2304
|
+
// src/v1/bridge/refresh-host-tool-catalog.ts
|
|
2305
|
+
async function refreshHostToolCatalog({
|
|
2306
|
+
relay,
|
|
2307
|
+
tools,
|
|
2308
|
+
harnessId,
|
|
2309
|
+
timeoutMs
|
|
2310
|
+
}) {
|
|
2311
|
+
const catalog = relay.updateCatalog({ tools });
|
|
2312
|
+
if (!catalog.changed && tools.length === 0) return;
|
|
2313
|
+
if (await relay.waitForCatalogRefresh({
|
|
2314
|
+
revision: catalog.revision,
|
|
2315
|
+
timeoutMs
|
|
2316
|
+
})) {
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
throw new HarnessBridgeCapabilityUnsupportedError({
|
|
2320
|
+
harnessId,
|
|
2321
|
+
message: `The ACP implementation did not load the active harness-owned MCP tool catalog to revision ${catalog.revision} before the next prompt, so host tools cannot be exposed safely.`
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// src/v1/bridge/permission-controller.ts
|
|
2326
|
+
var permissionRequestCounter = 0;
|
|
2327
|
+
function createACPPermissionController({
|
|
2328
|
+
turn,
|
|
2329
|
+
sessionId,
|
|
2330
|
+
permissionMode,
|
|
2331
|
+
hasPermissionModeMapping,
|
|
2332
|
+
emitToolCall,
|
|
2333
|
+
claimHostToolPermission
|
|
2334
|
+
}) {
|
|
2335
|
+
const pendingPermissions = /* @__PURE__ */ new Map();
|
|
2336
|
+
return {
|
|
2337
|
+
async requestPermission(request) {
|
|
2338
|
+
if (request.sessionId !== sessionId) {
|
|
2339
|
+
turn.emitWarning({
|
|
2340
|
+
message: `ACP permission request for session ${JSON.stringify(request.sessionId)} was cancelled because the active session is ${JSON.stringify(sessionId)}.`
|
|
2341
|
+
});
|
|
2342
|
+
return cancelled();
|
|
2343
|
+
}
|
|
2344
|
+
const allowOnce = request.options.find(
|
|
2345
|
+
(option) => option.kind === "allow_once"
|
|
2346
|
+
);
|
|
2347
|
+
const rejectOnce = request.options.find(
|
|
2348
|
+
(option) => option.kind === "reject_once"
|
|
2349
|
+
);
|
|
2350
|
+
if (allowOnce == null || rejectOnce == null) {
|
|
2351
|
+
const missing = [
|
|
2352
|
+
...allowOnce == null ? ["allow_once"] : [],
|
|
2353
|
+
...rejectOnce == null ? ["reject_once"] : []
|
|
2354
|
+
];
|
|
2355
|
+
turn.emitWarning({
|
|
2356
|
+
message: `ACP permission request for tool call ${JSON.stringify(request.toolCall.toolCallId)} was cancelled because it did not advertise ${missing.join(" and ")}.`
|
|
2357
|
+
});
|
|
2358
|
+
return cancelled();
|
|
2359
|
+
}
|
|
2360
|
+
if (claimHostToolPermission({ toolCall: request.toolCall })) {
|
|
2361
|
+
return {
|
|
2362
|
+
outcome: {
|
|
2363
|
+
outcome: "selected",
|
|
2364
|
+
optionId: allowOnce.optionId
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
if (!hasPermissionModeMapping && shouldAutoApprove({
|
|
2369
|
+
permissionMode,
|
|
2370
|
+
kind: request.toolCall.kind
|
|
2371
|
+
})) {
|
|
2372
|
+
return {
|
|
2373
|
+
outcome: {
|
|
2374
|
+
outcome: "selected",
|
|
2375
|
+
optionId: allowOnce.optionId
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
const approvalId = `acp-permission-${++permissionRequestCounter}`;
|
|
2380
|
+
let cancelPermission;
|
|
2381
|
+
const cancellation = new Promise((resolve) => {
|
|
2382
|
+
cancelPermission = () => resolve(void 0);
|
|
2383
|
+
});
|
|
2384
|
+
pendingPermissions.set(approvalId, {
|
|
2385
|
+
cancel: cancelPermission
|
|
2386
|
+
});
|
|
2387
|
+
const approval = turn.requestToolApproval(approvalId);
|
|
2388
|
+
emitToolCall({ toolCall: request.toolCall });
|
|
2389
|
+
turn.emit({
|
|
2390
|
+
type: "tool-approval-request",
|
|
2391
|
+
approvalId,
|
|
2392
|
+
toolCallId: request.toolCall.toolCallId
|
|
2393
|
+
});
|
|
2394
|
+
try {
|
|
2395
|
+
const response = await Promise.race([approval, cancellation]);
|
|
2396
|
+
if (response == null) return cancelled();
|
|
2397
|
+
return {
|
|
2398
|
+
outcome: {
|
|
2399
|
+
outcome: "selected",
|
|
2400
|
+
optionId: response.approved ? allowOnce.optionId : rejectOnce.optionId
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
} finally {
|
|
2404
|
+
pendingPermissions.delete(approvalId);
|
|
2405
|
+
}
|
|
2406
|
+
},
|
|
2407
|
+
cancelAll() {
|
|
2408
|
+
for (const pending of pendingPermissions.values()) {
|
|
2409
|
+
pending.cancel();
|
|
2410
|
+
}
|
|
2411
|
+
pendingPermissions.clear();
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
function shouldAutoApprove({
|
|
2416
|
+
permissionMode,
|
|
2417
|
+
kind
|
|
2418
|
+
}) {
|
|
2419
|
+
if (permissionMode === "allow-all") return true;
|
|
2420
|
+
if (kind === "read" || kind === "search" || kind === "think" || kind === "fetch") {
|
|
2421
|
+
return true;
|
|
2422
|
+
}
|
|
2423
|
+
return permissionMode === "allow-edits" && (kind === "edit" || kind === "delete" || kind === "move");
|
|
2424
|
+
}
|
|
2425
|
+
function cancelled() {
|
|
2426
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/v1/bridge/permission-mode.ts
|
|
2430
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
2431
|
+
var PERMISSION_MODES = [
|
|
2432
|
+
"allow-reads",
|
|
2433
|
+
"allow-edits",
|
|
2434
|
+
"allow-all"
|
|
2435
|
+
];
|
|
2436
|
+
async function configureACPPermissionMode({
|
|
2437
|
+
agent,
|
|
2438
|
+
sessionId,
|
|
2439
|
+
sessionConfiguration,
|
|
2440
|
+
permissionModeMapping,
|
|
2441
|
+
permissionMode,
|
|
2442
|
+
harnessId
|
|
2443
|
+
}) {
|
|
2444
|
+
const target = permissionModeMapping[permissionMode];
|
|
2445
|
+
if (target == null) {
|
|
2446
|
+
throw unsupported({
|
|
2447
|
+
harnessId,
|
|
2448
|
+
message: `Permission mode ${JSON.stringify(permissionMode)} is not supported by this ACP harness.` + (permissionModeMapping["allow-all"] == null ? "" : ` Use permissionMode: 'allow-all'.`)
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
for (const mappedPermissionMode of PERMISSION_MODES) {
|
|
2452
|
+
const mappedTarget = permissionModeMapping[mappedPermissionMode];
|
|
2453
|
+
if (mappedTarget != null) {
|
|
2454
|
+
validateTarget({
|
|
2455
|
+
target: mappedTarget,
|
|
2456
|
+
permissionMode: mappedPermissionMode,
|
|
2457
|
+
sessionConfiguration,
|
|
2458
|
+
harnessId
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
if (target.type === "session-mode") {
|
|
2463
|
+
await agent.request(acp.methods.agent.session.setMode, {
|
|
2464
|
+
sessionId,
|
|
2465
|
+
modeId: target.modeId
|
|
2466
|
+
});
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
await agent.request(acp.methods.agent.session.setConfigOption, {
|
|
2470
|
+
sessionId,
|
|
2471
|
+
configId: target.configId,
|
|
2472
|
+
value: target.value,
|
|
2473
|
+
...typeof target.value === "boolean" ? { type: "boolean" } : {}
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
function validateTarget({
|
|
2477
|
+
target,
|
|
2478
|
+
permissionMode,
|
|
2479
|
+
sessionConfiguration,
|
|
2480
|
+
harnessId
|
|
2481
|
+
}) {
|
|
2482
|
+
if (target.type === "session-mode") {
|
|
2483
|
+
const availableModeIds = sessionConfiguration.modes?.availableModes.map((mode) => mode.id) ?? [];
|
|
2484
|
+
if (!availableModeIds.includes(target.modeId)) {
|
|
2485
|
+
throw unsupported({
|
|
2486
|
+
harnessId,
|
|
2487
|
+
message: `ACP permission mapping for ${JSON.stringify(permissionMode)} requires session mode ${JSON.stringify(target.modeId)}, but the agent advertised ${formatChoices({ values: availableModeIds })}.`
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
const configOption = sessionConfiguration.configOptions?.find(
|
|
2493
|
+
(option) => option.id === target.configId
|
|
2494
|
+
);
|
|
2495
|
+
if (configOption == null) {
|
|
2496
|
+
const availableConfigIds = sessionConfiguration.configOptions?.map((option) => option.id) ?? [];
|
|
2497
|
+
throw unsupported({
|
|
2498
|
+
harnessId,
|
|
2499
|
+
message: `ACP permission mapping for ${JSON.stringify(permissionMode)} requires session configuration ${JSON.stringify(target.configId)}, but the agent advertised ${formatChoices({ values: availableConfigIds })}.`
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
if (configOption.type === "boolean") {
|
|
2503
|
+
if (typeof target.value !== "boolean") {
|
|
2504
|
+
throw unsupported({
|
|
2505
|
+
harnessId,
|
|
2506
|
+
message: `ACP permission mapping for ${JSON.stringify(permissionMode)} assigns string value ${JSON.stringify(target.value)} to boolean session configuration ${JSON.stringify(target.configId)}.`
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
if (typeof target.value !== "string") {
|
|
2512
|
+
throw unsupported({
|
|
2513
|
+
harnessId,
|
|
2514
|
+
message: `ACP permission mapping for ${JSON.stringify(permissionMode)} assigns boolean value ${JSON.stringify(target.value)} to select session configuration ${JSON.stringify(target.configId)}.`
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
const availableValues = configOption.options.flatMap(
|
|
2518
|
+
(option) => "options" in option ? option.options.map((value) => value.value) : [option.value]
|
|
2519
|
+
);
|
|
2520
|
+
if (!availableValues.includes(target.value)) {
|
|
2521
|
+
throw unsupported({
|
|
2522
|
+
harnessId,
|
|
2523
|
+
message: `ACP permission mapping for ${JSON.stringify(permissionMode)} requires value ${JSON.stringify(target.value)} for session configuration ${JSON.stringify(target.configId)}, but the agent advertised ${formatChoices({ values: availableValues })}.`
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
function formatChoices({ values }) {
|
|
2528
|
+
return values.length === 0 ? "no matching choices" : values.map((value) => JSON.stringify(value)).join(", ");
|
|
2529
|
+
}
|
|
2530
|
+
function unsupported({
|
|
2531
|
+
harnessId,
|
|
2532
|
+
message
|
|
2533
|
+
}) {
|
|
2534
|
+
return new HarnessBridgeCapabilityUnsupportedError({ harnessId, message });
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// src/v1/bridge/recovered-session.ts
|
|
2538
|
+
import * as acp2 from "@agentclientprotocol/sdk";
|
|
2539
|
+
function assertACPResumeCapability({
|
|
2540
|
+
initialization,
|
|
2541
|
+
harnessId
|
|
2542
|
+
}) {
|
|
2543
|
+
const sessionCapabilities = initialization.agentCapabilities?.sessionCapabilities;
|
|
2544
|
+
if (sessionCapabilities?.resume == null || sessionCapabilities.resume === false) {
|
|
2545
|
+
throw new HarnessBridgeCapabilityUnsupportedError({
|
|
2546
|
+
harnessId,
|
|
2547
|
+
message: "ACP process-loss rerun requires the agent to advertise sessionCapabilities.resume; a fresh unrelated ACP session will not be created."
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
function createACPRecoveredSession({
|
|
2552
|
+
agent,
|
|
2553
|
+
sessionId,
|
|
2554
|
+
restorationResponse,
|
|
2555
|
+
updates
|
|
2556
|
+
}) {
|
|
2557
|
+
let disposed = false;
|
|
2558
|
+
return {
|
|
2559
|
+
sessionId,
|
|
2560
|
+
newSessionResponse: {
|
|
2561
|
+
sessionId,
|
|
2562
|
+
...restorationResponse
|
|
2563
|
+
},
|
|
2564
|
+
prompt: async (prompt) => {
|
|
2565
|
+
if (disposed) {
|
|
2566
|
+
throw new Error("Recovered ACP session is disposed.");
|
|
2567
|
+
}
|
|
2568
|
+
updates.clearErrors();
|
|
2569
|
+
const response = agent.request(
|
|
2570
|
+
acp2.methods.agent.session.prompt,
|
|
2571
|
+
{ sessionId, prompt }
|
|
2572
|
+
);
|
|
2573
|
+
void response.then(
|
|
2574
|
+
(value) => {
|
|
2575
|
+
updates.enqueue({
|
|
2576
|
+
kind: "stop",
|
|
2577
|
+
response: value,
|
|
2578
|
+
stopReason: value.stopReason
|
|
2579
|
+
});
|
|
2580
|
+
},
|
|
2581
|
+
(error) => updates.reject(error)
|
|
2582
|
+
);
|
|
2583
|
+
return response;
|
|
2584
|
+
},
|
|
2585
|
+
nextUpdate: () => updates.next(),
|
|
2586
|
+
dispose: () => {
|
|
2587
|
+
if (disposed) return;
|
|
2588
|
+
disposed = true;
|
|
2589
|
+
updates.fail(new Error("Recovered ACP session disposed."));
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
function createACPRecoveredSessionUpdates() {
|
|
2594
|
+
const values = [];
|
|
2595
|
+
const waiters = [];
|
|
2596
|
+
let failure;
|
|
2597
|
+
let failed = false;
|
|
2598
|
+
return {
|
|
2599
|
+
enqueue(value) {
|
|
2600
|
+
if (failed) return;
|
|
2601
|
+
const waiter = waiters.shift();
|
|
2602
|
+
if (waiter == null) values.push({ type: "value", value });
|
|
2603
|
+
else waiter.resolve(value);
|
|
2604
|
+
},
|
|
2605
|
+
reject(error) {
|
|
2606
|
+
if (failed) return;
|
|
2607
|
+
const waiter = waiters.shift();
|
|
2608
|
+
if (waiter == null) values.push({ type: "error", error });
|
|
2609
|
+
else waiter.reject(error);
|
|
2610
|
+
},
|
|
2611
|
+
clearErrors() {
|
|
2612
|
+
for (let index = values.length - 1; index >= 0; index--) {
|
|
2613
|
+
if (values[index]?.type === "error") values.splice(index, 1);
|
|
2614
|
+
}
|
|
2615
|
+
},
|
|
2616
|
+
fail(error) {
|
|
2617
|
+
if (failed) return;
|
|
2618
|
+
failed = true;
|
|
2619
|
+
failure = error;
|
|
2620
|
+
for (const waiter of waiters.splice(0)) waiter.reject(error);
|
|
2621
|
+
},
|
|
2622
|
+
next() {
|
|
2623
|
+
const entry = values.shift();
|
|
2624
|
+
if (entry?.type === "value") return Promise.resolve(entry.value);
|
|
2625
|
+
if (entry?.type === "error") {
|
|
2626
|
+
return Promise.reject(
|
|
2627
|
+
entry.error instanceof Error ? entry.error : new Error("Recovered ACP session update failed.")
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
if (failed) {
|
|
2631
|
+
return Promise.reject(
|
|
2632
|
+
failure instanceof Error ? failure : new Error("Recovered ACP session update stream failed.")
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
return new Promise((resolve, reject) => {
|
|
2636
|
+
waiters.push({ resolve, reject });
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
// src/v1/bridge/session-lifecycle.ts
|
|
2643
|
+
import * as acp3 from "@agentclientprotocol/sdk";
|
|
2644
|
+
function resolveACPSessionRestorationMethod({
|
|
2645
|
+
initialization,
|
|
2646
|
+
harnessId
|
|
2647
|
+
}) {
|
|
2648
|
+
const sessionCapabilities = initialization.agentCapabilities?.sessionCapabilities;
|
|
2649
|
+
if (sessionCapabilities?.resume != null && sessionCapabilities.resume !== false) {
|
|
2650
|
+
return "resume";
|
|
2651
|
+
}
|
|
2652
|
+
if (initialization.agentCapabilities?.loadSession === true) {
|
|
2653
|
+
return "load";
|
|
2654
|
+
}
|
|
2655
|
+
throw new HarnessBridgeCapabilityUnsupportedError({
|
|
2656
|
+
harnessId,
|
|
2657
|
+
message: "Cold ACP session restoration requires the agent to advertise sessionCapabilities.resume or loadSession; a fresh unrelated ACP session will not be created."
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
async function restoreACPBridgeSession({
|
|
2661
|
+
agent,
|
|
2662
|
+
initialization,
|
|
2663
|
+
sessionId,
|
|
2664
|
+
cwd,
|
|
2665
|
+
mcpServers,
|
|
2666
|
+
meta,
|
|
2667
|
+
harnessId,
|
|
2668
|
+
setHistoricalUpdatesSuppressed,
|
|
2669
|
+
discardCapturedHistory
|
|
2670
|
+
}) {
|
|
2671
|
+
const method = resolveACPSessionRestorationMethod({
|
|
2672
|
+
initialization,
|
|
2673
|
+
harnessId
|
|
2674
|
+
});
|
|
2675
|
+
setHistoricalUpdatesSuppressed({ suppressed: true });
|
|
2676
|
+
try {
|
|
2677
|
+
const request = {
|
|
2678
|
+
sessionId,
|
|
2679
|
+
cwd,
|
|
2680
|
+
mcpServers: [...mcpServers],
|
|
2681
|
+
...meta == null ? {} : { _meta: meta }
|
|
2682
|
+
};
|
|
2683
|
+
const response = method === "resume" ? await agent.request(acp3.methods.agent.session.resume, request) : await agent.request(
|
|
2684
|
+
acp3.methods.agent.session.load,
|
|
2685
|
+
request
|
|
2686
|
+
);
|
|
2687
|
+
discardCapturedHistory();
|
|
2688
|
+
return { method, response };
|
|
2689
|
+
} finally {
|
|
2690
|
+
setHistoricalUpdatesSuppressed({ suppressed: false });
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// src/v1/bridge/index.ts
|
|
2695
|
+
var HOST_TOOL_MCP_SERVER_NAME = "ai-sdk-harness-tools";
|
|
2696
|
+
var CATALOG_REFRESH_TIMEOUT_MS = 1e4;
|
|
2697
|
+
var args = parseArgs({ args: argv.slice(2) });
|
|
2698
|
+
var workDir = requireArg({ value: args.workDir, name: "--workdir" });
|
|
2699
|
+
var bridgeStateDir = requireArg({
|
|
2700
|
+
value: args.bridgeStateDir,
|
|
2701
|
+
name: "--bridge-state-dir"
|
|
2702
|
+
});
|
|
2703
|
+
var implementationDir = requireArg({
|
|
2704
|
+
value: args.implementationDir,
|
|
2705
|
+
name: "--implementation-dir"
|
|
2706
|
+
});
|
|
2707
|
+
var bridgeType = requireArg({
|
|
2708
|
+
value: args.bridgeType,
|
|
2709
|
+
name: "--bridge-type"
|
|
2710
|
+
});
|
|
2711
|
+
var implementation = await readImplementationDescriptor({
|
|
2712
|
+
path: `${implementationDir}/implementation.json`
|
|
2713
|
+
});
|
|
2714
|
+
var bridgeConfiguration = await readACPBridgeEnvironment({ env: processEnv });
|
|
2715
|
+
var child;
|
|
2716
|
+
var connection;
|
|
2717
|
+
var session;
|
|
2718
|
+
var recoveredSessionUpdates;
|
|
2719
|
+
var recoveredSessionId;
|
|
2720
|
+
var sessionConfigurationFingerprint;
|
|
2721
|
+
var streamCapture;
|
|
2722
|
+
var initializationDiagnostic;
|
|
2723
|
+
var hostToolRelay;
|
|
2724
|
+
var catalogRefreshError;
|
|
2725
|
+
var sessionConfigurationFailure;
|
|
2726
|
+
var historicalUpdatesSuppressed = false;
|
|
2727
|
+
var coldRestorationMethod;
|
|
2728
|
+
var activePermissionController;
|
|
2729
|
+
await runBridge({
|
|
2730
|
+
bridgeType,
|
|
2731
|
+
bridgeStateDir,
|
|
2732
|
+
onStart: runTurn,
|
|
2733
|
+
onStop: () => session == null ? {} : { sessionId: session.sessionId },
|
|
2734
|
+
onExit: () => {
|
|
2735
|
+
let exited = false;
|
|
2736
|
+
const finish = () => {
|
|
2737
|
+
if (exited) return;
|
|
2738
|
+
exited = true;
|
|
2739
|
+
session?.dispose();
|
|
2740
|
+
connection?.close();
|
|
2741
|
+
child?.kill();
|
|
2742
|
+
process.exit(0);
|
|
2743
|
+
};
|
|
2744
|
+
if (hostToolRelay == null) {
|
|
2745
|
+
finish();
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
const timer = setTimeout(finish, 1e3);
|
|
2749
|
+
timer.unref();
|
|
2750
|
+
void hostToolRelay.close().finally(() => {
|
|
2751
|
+
clearTimeout(timer);
|
|
2752
|
+
finish();
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
});
|
|
2756
|
+
async function runTurn(start, turn) {
|
|
2757
|
+
try {
|
|
2758
|
+
await ensureSession({ start, turn });
|
|
2759
|
+
} catch (error) {
|
|
2760
|
+
if (HarnessBridgeCapabilityUnsupportedError.isInstance(error)) throw error;
|
|
2761
|
+
throw createACPBridgeError({
|
|
2762
|
+
stage: "session initialization",
|
|
2763
|
+
cause: error
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
const activeSession = session;
|
|
2767
|
+
if (activeSession == null) {
|
|
2768
|
+
throw new Error("ACP session initialization did not produce a session.");
|
|
2769
|
+
}
|
|
2770
|
+
if (start.recoveryMode?.type === "lossy-rerun") {
|
|
2771
|
+
const marker = {
|
|
2772
|
+
type: "acp-recovery",
|
|
2773
|
+
mode: "lossy-rerun",
|
|
2774
|
+
reason: start.recoveryMode.reason
|
|
2775
|
+
};
|
|
2776
|
+
turn.emit({ type: "raw", rawValue: marker });
|
|
2777
|
+
turn.bridgeLog({
|
|
2778
|
+
level: "warn",
|
|
2779
|
+
subsystem: "acp.recovery",
|
|
2780
|
+
message: "The ACP process was replaced; the original prompt is being rerun against the resumed ACP session.",
|
|
2781
|
+
attrs: marker
|
|
2782
|
+
});
|
|
2783
|
+
turn.emitWarning({
|
|
2784
|
+
message: "ACP process-loss recovery is rerunning the interrupted prompt and may repeat work."
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
turn.emit({
|
|
2788
|
+
type: "bridge-thread",
|
|
2789
|
+
threadId: activeSession.sessionId
|
|
2790
|
+
});
|
|
2791
|
+
if (initializationDiagnostic != null) {
|
|
2792
|
+
turn.bridgeLog({
|
|
2793
|
+
level: "info",
|
|
2794
|
+
subsystem: "acp.protocol",
|
|
2795
|
+
message: "ACP session initialized.",
|
|
2796
|
+
attrs: initializationDiagnostic
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
if (start.recoveryMode?.type === "cold-restore") {
|
|
2800
|
+
const marker = {
|
|
2801
|
+
type: "acp-session-restored",
|
|
2802
|
+
method: coldRestorationMethod
|
|
2803
|
+
};
|
|
2804
|
+
turn.emit({ type: "raw", rawValue: marker });
|
|
2805
|
+
turn.bridgeLog({
|
|
2806
|
+
level: "info",
|
|
2807
|
+
subsystem: "acp.lifecycle",
|
|
2808
|
+
message: "ACP session restored in a replacement bridge process.",
|
|
2809
|
+
attrs: marker
|
|
2810
|
+
});
|
|
2811
|
+
turn.emit({
|
|
2812
|
+
type: "finish",
|
|
2813
|
+
finishReason: {
|
|
2814
|
+
unified: "stop",
|
|
2815
|
+
raw: "acp-session-restored"
|
|
2816
|
+
},
|
|
2817
|
+
totalUsage: unknownUsage2()
|
|
2818
|
+
});
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
let rejectCancellationFailure;
|
|
2822
|
+
const cancellationFailure = new Promise((_, reject) => {
|
|
2823
|
+
rejectCancellationFailure = reject;
|
|
2824
|
+
});
|
|
2825
|
+
void cancellationFailure.catch(() => {
|
|
2826
|
+
});
|
|
2827
|
+
let cancellationRequested = false;
|
|
2828
|
+
let cancellationFailureError;
|
|
2829
|
+
const cancel = async () => {
|
|
2830
|
+
if (cancellationRequested) return;
|
|
2831
|
+
cancellationRequested = true;
|
|
2832
|
+
activePermissionController?.cancelAll();
|
|
2833
|
+
try {
|
|
2834
|
+
if (connection == null) {
|
|
2835
|
+
throw new Error("ACP connection closed before cancellation.");
|
|
2836
|
+
}
|
|
2837
|
+
await connection.agent.notify(acp4.methods.agent.session.cancel, {
|
|
2838
|
+
sessionId: activeSession.sessionId
|
|
2839
|
+
});
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
cancellationFailureError = createACPBridgeError({
|
|
2842
|
+
stage: "session cancellation",
|
|
2843
|
+
cause: error
|
|
2844
|
+
});
|
|
2845
|
+
rejectCancellationFailure(cancellationFailureError);
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
turn.emit({ type: "stream-start" });
|
|
2849
|
+
const emitStreamEvent = createEmitStreamEvent({
|
|
2850
|
+
emit: (event) => turn.emit(event),
|
|
2851
|
+
builtinTools: start.builtinTools,
|
|
2852
|
+
hostToolServerName: HOST_TOOL_MCP_SERVER_NAME,
|
|
2853
|
+
hostTools: start.tools ?? []
|
|
2854
|
+
});
|
|
2855
|
+
const permissionController = createACPPermissionController({
|
|
2856
|
+
turn,
|
|
2857
|
+
sessionId: activeSession.sessionId,
|
|
2858
|
+
permissionMode: start.permissionMode ?? "allow-all",
|
|
2859
|
+
hasPermissionModeMapping: start.permissionModeMapping != null,
|
|
2860
|
+
emitToolCall: emitStreamEvent.permissionToolCall,
|
|
2861
|
+
claimHostToolPermission: emitStreamEvent.claimHostToolPermission
|
|
2862
|
+
});
|
|
2863
|
+
activePermissionController = permissionController;
|
|
2864
|
+
const relayTurn = {
|
|
2865
|
+
emitToolCall: emitStreamEvent.hostToolCall,
|
|
2866
|
+
emitToolResult: emitStreamEvent.hostToolResult,
|
|
2867
|
+
requestToolResult: (toolCallId) => turn.requestToolResult(toolCallId),
|
|
2868
|
+
registerCorrelationInvocation: emitStreamEvent.registerHostToolCorrelationInvocation,
|
|
2869
|
+
removeCorrelationInvocation: emitStreamEvent.removeHostToolCorrelationInvocation
|
|
2870
|
+
};
|
|
2871
|
+
hostToolRelay?.bindTurn({ turn: relayTurn });
|
|
2872
|
+
try {
|
|
2873
|
+
void activeSession.prompt(start.prompt);
|
|
2874
|
+
if (turn.abortSignal.aborted) {
|
|
2875
|
+
await cancel();
|
|
2876
|
+
} else {
|
|
2877
|
+
turn.abortSignal.addEventListener("abort", () => void cancel(), {
|
|
2878
|
+
once: true
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
for (; ; ) {
|
|
2882
|
+
let message;
|
|
2883
|
+
try {
|
|
2884
|
+
message = await Promise.race([
|
|
2885
|
+
activeSession.nextUpdate(),
|
|
2886
|
+
cancellationFailure
|
|
2887
|
+
]);
|
|
2888
|
+
} catch (error) {
|
|
2889
|
+
for (const rawValue of streamCapture?.drainRawValues() ?? []) {
|
|
2890
|
+
emitStreamEvent.raw({ rawValue });
|
|
2891
|
+
}
|
|
2892
|
+
emitStreamEvent.close();
|
|
2893
|
+
if (error === cancellationFailureError) throw error;
|
|
2894
|
+
throw createACPBridgeError({
|
|
2895
|
+
stage: "prompt update stream",
|
|
2896
|
+
cause: error
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
if (message.kind === "session_update") {
|
|
2900
|
+
const captured = streamCapture?.takeForUpdate({
|
|
2901
|
+
update: message.update
|
|
2902
|
+
});
|
|
2903
|
+
for (const rawValue of captured?.precedingRawValues ?? []) {
|
|
2904
|
+
emitStreamEvent.raw({ rawValue });
|
|
2905
|
+
}
|
|
2906
|
+
emitStreamEvent.message({
|
|
2907
|
+
message,
|
|
2908
|
+
rawUpdate: captured?.rawUpdate
|
|
2909
|
+
});
|
|
2910
|
+
continue;
|
|
2911
|
+
}
|
|
2912
|
+
for (const rawValue of streamCapture?.drainRawValues() ?? []) {
|
|
2913
|
+
emitStreamEvent.raw({ rawValue });
|
|
2914
|
+
}
|
|
2915
|
+
if (emitStreamEvent.message({ message })) return;
|
|
2916
|
+
}
|
|
2917
|
+
} finally {
|
|
2918
|
+
permissionController.cancelAll();
|
|
2919
|
+
activePermissionController = void 0;
|
|
2920
|
+
hostToolRelay?.unbindTurn({ turn: relayTurn });
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
async function ensureSession({
|
|
2924
|
+
start,
|
|
2925
|
+
turn
|
|
2926
|
+
}) {
|
|
2927
|
+
if (sessionConfigurationFailure != null) {
|
|
2928
|
+
throw sessionConfigurationFailure.error;
|
|
2929
|
+
}
|
|
2930
|
+
const fingerprint = JSON.stringify({
|
|
2931
|
+
authentication: bridgeConfiguration.authentication,
|
|
2932
|
+
providerAuthentication: bridgeConfiguration.providerAuthentication,
|
|
2933
|
+
sessionMeta: bridgeConfiguration.sessionMeta,
|
|
2934
|
+
permissionMode: start.permissionMode,
|
|
2935
|
+
permissionModeMapping: start.permissionModeMapping
|
|
2936
|
+
});
|
|
2937
|
+
if (session != null) {
|
|
2938
|
+
if (catalogRefreshError != null) throw catalogRefreshError;
|
|
2939
|
+
if (sessionConfigurationFingerprint !== fingerprint) {
|
|
2940
|
+
throw new Error(
|
|
2941
|
+
"ACP authentication and session profile settings cannot change after the ACP session has started."
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
const relay = hostToolRelay;
|
|
2945
|
+
if (relay == null) {
|
|
2946
|
+
throw new Error("The host tool MCP relay is unavailable.");
|
|
2947
|
+
}
|
|
2948
|
+
try {
|
|
2949
|
+
await refreshHostToolCatalog({
|
|
2950
|
+
relay,
|
|
2951
|
+
tools: start.tools ?? [],
|
|
2952
|
+
harnessId: bridgeType,
|
|
2953
|
+
timeoutMs: CATALOG_REFRESH_TIMEOUT_MS
|
|
2954
|
+
});
|
|
2955
|
+
} catch (error) {
|
|
2956
|
+
if (HarnessBridgeCapabilityUnsupportedError.isInstance(error)) {
|
|
2957
|
+
catalogRefreshError = error;
|
|
2958
|
+
}
|
|
2959
|
+
throw error;
|
|
2960
|
+
}
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
const clientApp = resolveClientApp();
|
|
2964
|
+
const gateway = bridgeConfiguration.providerAuthentication?.type === "ai-gateway" ? resolveGatewayValues({ clientApp }) : void 0;
|
|
2965
|
+
const authentication = bridgeConfiguration.authentication;
|
|
2966
|
+
const launchEnv = resolveACPLaunchEnvironment({
|
|
2967
|
+
providerAuthentication: bridgeConfiguration.providerAuthentication,
|
|
2968
|
+
gateway
|
|
2969
|
+
});
|
|
2970
|
+
child = spawn(
|
|
2971
|
+
`${implementationDir}/node_modules/.bin/${implementation.executable}`,
|
|
2972
|
+
[...implementation.args],
|
|
2973
|
+
{
|
|
2974
|
+
cwd: workDir,
|
|
2975
|
+
env: createChildEnvironment({ launchEnv }),
|
|
2976
|
+
shell: false,
|
|
2977
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2978
|
+
}
|
|
2979
|
+
);
|
|
2980
|
+
forwardAgentStderr({ child, turn });
|
|
2981
|
+
const input = Writable.toWeb(child.stdin);
|
|
2982
|
+
const output = Readable.toWeb(child.stdout);
|
|
2983
|
+
const capturedStream = captureACPStream({
|
|
2984
|
+
stream: acp4.ndJsonStream(input, output)
|
|
2985
|
+
});
|
|
2986
|
+
streamCapture = capturedStream.capture;
|
|
2987
|
+
connection = acp4.client({ name: clientApp.name }).onRequest(
|
|
2988
|
+
acp4.methods.client.session.requestPermission,
|
|
2989
|
+
({ params }) => activePermissionController?.requestPermission(params) ?? {
|
|
2990
|
+
outcome: { outcome: "cancelled" }
|
|
2991
|
+
}
|
|
2992
|
+
).onNotification(acp4.methods.client.session.update, ({ params }) => {
|
|
2993
|
+
if (params.sessionId !== recoveredSessionId) return;
|
|
2994
|
+
if (historicalUpdatesSuppressed) return;
|
|
2995
|
+
recoveredSessionUpdates?.enqueue({
|
|
2996
|
+
kind: "session_update",
|
|
2997
|
+
notification: params,
|
|
2998
|
+
update: params.update
|
|
2999
|
+
});
|
|
3000
|
+
}).connect(capturedStream.stream);
|
|
3001
|
+
const initializeRequest = createACPInitializeRequest({
|
|
3002
|
+
protocolVersion: acp4.PROTOCOL_VERSION,
|
|
3003
|
+
clientApp,
|
|
3004
|
+
authentication,
|
|
3005
|
+
supportsBooleanSessionConfigOptions: Object.values(
|
|
3006
|
+
start.permissionModeMapping ?? {}
|
|
3007
|
+
).some(
|
|
3008
|
+
(target) => target?.type === "session-config-option" && typeof target.value === "boolean"
|
|
3009
|
+
)
|
|
3010
|
+
});
|
|
3011
|
+
const initialization = await connection.agent.request(acp4.methods.agent.initialize, initializeRequest);
|
|
3012
|
+
validateACPProtocolVersion({
|
|
3013
|
+
requested: acp4.PROTOCOL_VERSION,
|
|
3014
|
+
initialization
|
|
3015
|
+
});
|
|
3016
|
+
if (authentication != null) {
|
|
3017
|
+
await authenticate({
|
|
3018
|
+
agent: connection.agent,
|
|
3019
|
+
initialization,
|
|
3020
|
+
authentication
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
const tools = start.tools ?? [];
|
|
3024
|
+
const catalogPath = `${bridgeStateDir}/host-tools.json`;
|
|
3025
|
+
await writeFile2(catalogPath, JSON.stringify(tools), { mode: 384 });
|
|
3026
|
+
hostToolRelay = await startHostToolRelay({
|
|
3027
|
+
tools,
|
|
3028
|
+
serverName: HOST_TOOL_MCP_SERVER_NAME
|
|
3029
|
+
});
|
|
3030
|
+
const mcpServers = [
|
|
3031
|
+
{
|
|
3032
|
+
name: HOST_TOOL_MCP_SERVER_NAME,
|
|
3033
|
+
command: process.execPath,
|
|
3034
|
+
args: [fileURLToPath(new URL("./host-tool-mcp.mjs", import.meta.url))],
|
|
3035
|
+
env: [
|
|
3036
|
+
{
|
|
3037
|
+
name: "AI_SDK_ACP_HOST_TOOLS_FILE",
|
|
3038
|
+
value: catalogPath
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
name: "AI_SDK_ACP_HOST_TOOL_RELAY_URL",
|
|
3042
|
+
value: hostToolRelay.url
|
|
3043
|
+
},
|
|
3044
|
+
{
|
|
3045
|
+
name: "AI_SDK_ACP_HOST_TOOL_RELAY_CREDENTIAL",
|
|
3046
|
+
value: hostToolRelay.credential
|
|
3047
|
+
}
|
|
3048
|
+
]
|
|
3049
|
+
}
|
|
3050
|
+
];
|
|
3051
|
+
let createdSession;
|
|
3052
|
+
if (start.recoveryMode?.type === "lossy-rerun") {
|
|
3053
|
+
assertACPResumeCapability({ initialization, harnessId: bridgeType });
|
|
3054
|
+
recoveredSessionId = start.recoveryMode.acpSessionId;
|
|
3055
|
+
recoveredSessionUpdates = createACPRecoveredSessionUpdates();
|
|
3056
|
+
const resumeResponse = await connection.agent.request(acp4.methods.agent.session.resume, {
|
|
3057
|
+
sessionId: recoveredSessionId,
|
|
3058
|
+
cwd: workDir,
|
|
3059
|
+
mcpServers,
|
|
3060
|
+
...bridgeConfiguration.sessionMeta == null ? {} : { _meta: bridgeConfiguration.sessionMeta }
|
|
3061
|
+
});
|
|
3062
|
+
createdSession = createACPRecoveredSession({
|
|
3063
|
+
agent: connection.agent,
|
|
3064
|
+
sessionId: recoveredSessionId,
|
|
3065
|
+
restorationResponse: resumeResponse,
|
|
3066
|
+
updates: recoveredSessionUpdates
|
|
3067
|
+
});
|
|
3068
|
+
} else if (start.recoveryMode?.type === "cold-restore") {
|
|
3069
|
+
recoveredSessionId = start.recoveryMode.acpSessionId;
|
|
3070
|
+
recoveredSessionUpdates = createACPRecoveredSessionUpdates();
|
|
3071
|
+
const restored = await restoreACPBridgeSession({
|
|
3072
|
+
agent: connection.agent,
|
|
3073
|
+
initialization,
|
|
3074
|
+
sessionId: recoveredSessionId,
|
|
3075
|
+
cwd: workDir,
|
|
3076
|
+
mcpServers,
|
|
3077
|
+
meta: bridgeConfiguration.sessionMeta,
|
|
3078
|
+
harnessId: bridgeType,
|
|
3079
|
+
setHistoricalUpdatesSuppressed: ({ suppressed }) => {
|
|
3080
|
+
historicalUpdatesSuppressed = suppressed;
|
|
3081
|
+
},
|
|
3082
|
+
discardCapturedHistory: () => {
|
|
3083
|
+
streamCapture?.drainRawValues();
|
|
3084
|
+
}
|
|
3085
|
+
});
|
|
3086
|
+
coldRestorationMethod = restored.method;
|
|
3087
|
+
createdSession = createACPRecoveredSession({
|
|
3088
|
+
agent: connection.agent,
|
|
3089
|
+
sessionId: recoveredSessionId,
|
|
3090
|
+
restorationResponse: restored.response,
|
|
3091
|
+
updates: recoveredSessionUpdates
|
|
3092
|
+
});
|
|
3093
|
+
} else {
|
|
3094
|
+
createdSession = await connection.agent.buildSession({
|
|
3095
|
+
cwd: workDir,
|
|
3096
|
+
mcpServers,
|
|
3097
|
+
...bridgeConfiguration.sessionMeta == null ? {} : { _meta: bridgeConfiguration.sessionMeta }
|
|
3098
|
+
}).start();
|
|
3099
|
+
}
|
|
3100
|
+
try {
|
|
3101
|
+
await refreshHostToolCatalog({
|
|
3102
|
+
relay: hostToolRelay,
|
|
3103
|
+
tools,
|
|
3104
|
+
harnessId: bridgeType,
|
|
3105
|
+
timeoutMs: CATALOG_REFRESH_TIMEOUT_MS
|
|
3106
|
+
});
|
|
3107
|
+
if (start.permissionModeMapping != null) {
|
|
3108
|
+
await configureACPPermissionMode({
|
|
3109
|
+
agent: connection.agent,
|
|
3110
|
+
sessionId: createdSession.sessionId,
|
|
3111
|
+
sessionConfiguration: createdSession.newSessionResponse,
|
|
3112
|
+
permissionModeMapping: start.permissionModeMapping,
|
|
3113
|
+
permissionMode: start.permissionMode ?? "allow-all",
|
|
3114
|
+
harnessId: bridgeType
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
} catch (error) {
|
|
3118
|
+
createdSession.dispose();
|
|
3119
|
+
if (HarnessBridgeCapabilityUnsupportedError.isInstance(error)) {
|
|
3120
|
+
catalogRefreshError = error;
|
|
3121
|
+
}
|
|
3122
|
+
sessionConfigurationFailure = { error };
|
|
3123
|
+
throw error;
|
|
3124
|
+
}
|
|
3125
|
+
session = createdSession;
|
|
3126
|
+
initializationDiagnostic = createACPInitializationDiagnostic({
|
|
3127
|
+
initialization,
|
|
3128
|
+
sessionId: createdSession.sessionId
|
|
3129
|
+
});
|
|
3130
|
+
sessionConfigurationFingerprint = fingerprint;
|
|
3131
|
+
}
|
|
3132
|
+
function unknownUsage2() {
|
|
3133
|
+
return {
|
|
3134
|
+
inputTokens: {
|
|
3135
|
+
total: void 0,
|
|
3136
|
+
noCache: void 0,
|
|
3137
|
+
cacheRead: void 0,
|
|
3138
|
+
cacheWrite: void 0
|
|
3139
|
+
},
|
|
3140
|
+
outputTokens: {
|
|
3141
|
+
total: void 0,
|
|
3142
|
+
text: void 0,
|
|
3143
|
+
reasoning: void 0
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
async function authenticate({
|
|
3148
|
+
agent,
|
|
3149
|
+
initialization,
|
|
3150
|
+
authentication
|
|
3151
|
+
}) {
|
|
3152
|
+
assertACPAuthenticationMethod({
|
|
3153
|
+
initialization,
|
|
3154
|
+
methodId: authentication.methodId
|
|
3155
|
+
});
|
|
3156
|
+
await agent.request(acp4.methods.agent.authenticate, {
|
|
3157
|
+
methodId: authentication.methodId,
|
|
3158
|
+
...authentication.meta == null ? {} : { _meta: authentication.meta }
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
function resolveClientApp() {
|
|
3162
|
+
const name2 = processEnv.AI_SDK_ACP_CLIENT_APP_NAME;
|
|
3163
|
+
const version = processEnv.AI_SDK_ACP_CLIENT_APP_VERSION;
|
|
3164
|
+
if (name2 == null || version == null) {
|
|
3165
|
+
throw new Error("ACP client app values were not supplied to the bridge.");
|
|
3166
|
+
}
|
|
3167
|
+
return { name: name2, version };
|
|
3168
|
+
}
|
|
3169
|
+
function resolveGatewayValues({
|
|
3170
|
+
clientApp
|
|
3171
|
+
}) {
|
|
3172
|
+
const apiKey = processEnv.AI_SDK_ACP_GATEWAY_API_KEY;
|
|
3173
|
+
const baseUrl = processEnv.AI_SDK_ACP_GATEWAY_BASE_URL;
|
|
3174
|
+
if (apiKey == null || baseUrl == null) {
|
|
3175
|
+
throw new Error(
|
|
3176
|
+
"AI Gateway profile values were not supplied to the ACP bridge."
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
return {
|
|
3180
|
+
apiKey,
|
|
3181
|
+
baseUrl,
|
|
3182
|
+
clientAppName: clientApp.name,
|
|
3183
|
+
clientAppVersion: clientApp.version
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
function createChildEnvironment({
|
|
3187
|
+
launchEnv
|
|
3188
|
+
}) {
|
|
3189
|
+
const blocked = /* @__PURE__ */ new Set([
|
|
3190
|
+
"BRIDGE_CHANNEL_TOKEN",
|
|
3191
|
+
"BRIDGE_WS_PORT",
|
|
3192
|
+
"AI_SDK_ACP_GATEWAY_API_KEY",
|
|
3193
|
+
"AI_SDK_ACP_GATEWAY_BASE_URL",
|
|
3194
|
+
"AI_SDK_ACP_CLIENT_APP_NAME",
|
|
3195
|
+
"AI_SDK_ACP_CLIENT_APP_VERSION",
|
|
3196
|
+
ACP_BRIDGE_CONFIGURATION_ENV
|
|
3197
|
+
]);
|
|
3198
|
+
return {
|
|
3199
|
+
...Object.fromEntries(
|
|
3200
|
+
Object.entries(processEnv).filter(
|
|
3201
|
+
([key, value]) => !blocked.has(key) && value != null
|
|
3202
|
+
)
|
|
3203
|
+
),
|
|
3204
|
+
...launchEnv
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
function isRecord6(value) {
|
|
3208
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
3209
|
+
}
|
|
3210
|
+
function forwardAgentStderr({
|
|
3211
|
+
child: child2,
|
|
3212
|
+
turn
|
|
3213
|
+
}) {
|
|
3214
|
+
void (async () => {
|
|
3215
|
+
child2.stderr.setEncoding("utf8");
|
|
3216
|
+
let pending = "";
|
|
3217
|
+
for await (const chunk of child2.stderr) {
|
|
3218
|
+
pending += chunk;
|
|
3219
|
+
const lines = pending.split("\n");
|
|
3220
|
+
pending = lines.pop() ?? "";
|
|
3221
|
+
for (const line of lines) {
|
|
3222
|
+
if (line.length === 0) continue;
|
|
3223
|
+
turn.bridgeLog({
|
|
3224
|
+
level: "warn",
|
|
3225
|
+
subsystem: "acp.agent.stderr",
|
|
3226
|
+
message: line
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
if (pending.length > 0) {
|
|
3231
|
+
turn.bridgeLog({
|
|
3232
|
+
level: "warn",
|
|
3233
|
+
subsystem: "acp.agent.stderr",
|
|
3234
|
+
message: pending
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
})();
|
|
3238
|
+
}
|
|
3239
|
+
async function readImplementationDescriptor({
|
|
3240
|
+
path
|
|
3241
|
+
}) {
|
|
3242
|
+
const text = await readFile(path, "utf8");
|
|
3243
|
+
const value = await new Response(text, {
|
|
3244
|
+
headers: { "content-type": "application/json" }
|
|
3245
|
+
}).json();
|
|
3246
|
+
if (!isRecord6(value) || typeof value.executable !== "string" || !Array.isArray(value.args) || !value.args.every((item) => typeof item === "string") || !Array.isArray(value.envKeys) || !value.envKeys.every((item) => typeof item === "string")) {
|
|
3247
|
+
throw new Error("Invalid ACP implementation descriptor.");
|
|
3248
|
+
}
|
|
3249
|
+
return {
|
|
3250
|
+
executable: value.executable,
|
|
3251
|
+
args: value.args,
|
|
3252
|
+
envKeys: value.envKeys
|
|
3253
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
function parseArgs({ args: args2 }) {
|
|
3256
|
+
const result = {};
|
|
3257
|
+
for (let index = 0; index < args2.length; index++) {
|
|
3258
|
+
const value = args2[index + 1];
|
|
3259
|
+
if (value == null) continue;
|
|
3260
|
+
if (args2[index] === "--workdir") result.workDir = value;
|
|
3261
|
+
else if (args2[index] === "--bridge-state-dir")
|
|
3262
|
+
result.bridgeStateDir = value;
|
|
3263
|
+
else if (args2[index] === "--implementation-dir")
|
|
3264
|
+
result.implementationDir = value;
|
|
3265
|
+
else if (args2[index] === "--bridge-type") result.bridgeType = value;
|
|
3266
|
+
}
|
|
3267
|
+
return result;
|
|
3268
|
+
}
|
|
3269
|
+
function requireArg({
|
|
3270
|
+
value,
|
|
3271
|
+
name: name2
|
|
3272
|
+
}) {
|
|
3273
|
+
if (value == null || value.length === 0) {
|
|
3274
|
+
throw new Error(`Missing ${name2} argument.`);
|
|
3275
|
+
}
|
|
3276
|
+
return value;
|
|
3277
|
+
}
|
|
3278
|
+
//# sourceMappingURL=index.mjs.map
|