@wrongstack/acp 0.274.0 → 0.275.1
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/README.md +376 -0
- package/dist/acp-subagent-runner-BAlo23L-.d.ts +644 -0
- package/dist/acp-v1-BxskPsdo.d.ts +520 -0
- package/dist/agent.d.ts +34 -61
- package/dist/agent.js +796 -32
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +3 -2
- package/dist/client.js +779 -112
- package/dist/client.js.map +1 -1
- package/dist/index-DEEYyEpu.d.ts +54 -0
- package/dist/index.d.ts +186 -227
- package/dist/index.js +1881 -286
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +12 -0
- package/dist/sdk.js +3350 -0
- package/dist/sdk.js.map +1 -0
- package/dist/server-agent-turn-C3U0lhA-.d.ts +163 -0
- package/dist/terminal-server-P9KpMZTT.d.ts +99 -0
- package/dist/{tools-registry-BCf8evEG.d.ts → tools-registry-D2xdbzN7.d.ts} +1 -1
- package/dist/wrongstack-acp-agent-nzrqmJnc.d.ts +341 -0
- package/dist/wrongstack-acp-agent.d.ts +2 -2
- package/dist/wrongstack-acp-agent.js +426 -26
- package/dist/wrongstack-acp-agent.js.map +1 -1
- package/package.json +7 -2
- package/dist/index-BvPqJHhm.d.ts +0 -119
- package/dist/stdio-transport-CsFr8JzC.d.ts +0 -205
- package/dist/wrongstack-acp-agent-Dv-A0bEm.d.ts +0 -310
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,3350 @@
|
|
|
1
|
+
export { AGENT_METHODS, ActiveSession, AgentApp, CLIENT_METHODS, ClientApp, PROTOCOL_METHODS, PROTOCOL_VERSION, SessionBuilder, methods } from '@agentclientprotocol/sdk';
|
|
2
|
+
export { AcpServer } from '@agentclientprotocol/sdk/experimental/server';
|
|
3
|
+
export { createWebSocketStream } from '@agentclientprotocol/sdk/experimental/ws-client';
|
|
4
|
+
export { createNodeHttpHandler, createNodeWebSocketUpgradeHandler } from '@agentclientprotocol/sdk/experimental/node';
|
|
5
|
+
import { writeErr, expectDefined } from '@wrongstack/core';
|
|
6
|
+
import * as fsp2 from 'fs/promises';
|
|
7
|
+
import * as path3 from 'path';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { createServer } from 'http';
|
|
11
|
+
import { SubagentBudget } from '@wrongstack/core/coordination';
|
|
12
|
+
|
|
13
|
+
// src/sdk.ts
|
|
14
|
+
var StdioTransport = class {
|
|
15
|
+
stdin = process.stdin;
|
|
16
|
+
stdout = process.stdout;
|
|
17
|
+
stderr = process.stderr;
|
|
18
|
+
buffer = "";
|
|
19
|
+
handlers = /* @__PURE__ */ new Set();
|
|
20
|
+
closed = false;
|
|
21
|
+
resolveRead = null;
|
|
22
|
+
messageQueue = [];
|
|
23
|
+
constructor() {
|
|
24
|
+
this.stdin.resume();
|
|
25
|
+
this.stdin.setEncoding("utf8");
|
|
26
|
+
this.stdin.on("data", (chunk) => this.onData(chunk));
|
|
27
|
+
this.stdin.on("end", () => this.handleClose());
|
|
28
|
+
this.stdin.on("error", (err) => this.failAll(err));
|
|
29
|
+
}
|
|
30
|
+
sendStartupMarker() {
|
|
31
|
+
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
32
|
+
}
|
|
33
|
+
send(msg) {
|
|
34
|
+
if (this.closed) return Promise.resolve();
|
|
35
|
+
return new Promise((resolve3) => {
|
|
36
|
+
const line = JSON.stringify(msg) + "\n";
|
|
37
|
+
this.stdout.write(line, "utf8", () => resolve3());
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
sendRaw(chunk) {
|
|
41
|
+
this.stdout.write(chunk, "utf8");
|
|
42
|
+
}
|
|
43
|
+
read() {
|
|
44
|
+
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
45
|
+
if (this.closed) return Promise.resolve(null);
|
|
46
|
+
return new Promise((resolve3) => {
|
|
47
|
+
this.resolveRead = resolve3;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
onMessage(handler) {
|
|
51
|
+
this.handlers.add(handler);
|
|
52
|
+
return () => this.handlers.delete(handler);
|
|
53
|
+
}
|
|
54
|
+
close() {
|
|
55
|
+
this.closed = true;
|
|
56
|
+
this.stdin.pause();
|
|
57
|
+
this.resolveRead?.(null);
|
|
58
|
+
this.resolveRead = null;
|
|
59
|
+
}
|
|
60
|
+
onData(chunk) {
|
|
61
|
+
this.buffer += chunk;
|
|
62
|
+
const lines = this.buffer.split("\n");
|
|
63
|
+
this.buffer = lines.pop() ?? "";
|
|
64
|
+
for (const raw of lines) {
|
|
65
|
+
if (!raw.trim()) continue;
|
|
66
|
+
try {
|
|
67
|
+
this.dispatch(JSON.parse(raw));
|
|
68
|
+
} catch (err) {
|
|
69
|
+
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
70
|
+
`, "utf8");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
dispatch(msg) {
|
|
75
|
+
if (this.resolveRead) {
|
|
76
|
+
const resolve3 = this.resolveRead;
|
|
77
|
+
this.resolveRead = null;
|
|
78
|
+
resolve3(msg);
|
|
79
|
+
} else {
|
|
80
|
+
this.messageQueue.push(msg);
|
|
81
|
+
}
|
|
82
|
+
for (const handler of this.handlers) {
|
|
83
|
+
try {
|
|
84
|
+
handler(msg);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
87
|
+
`, "utf8");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
handleClose() {
|
|
92
|
+
this.closed = true;
|
|
93
|
+
this.resolveRead?.(null);
|
|
94
|
+
this.resolveRead = null;
|
|
95
|
+
}
|
|
96
|
+
failAll(err) {
|
|
97
|
+
this.stderr.write(`[wstack-acp stdin error] ${err.message}
|
|
98
|
+
`, "utf8");
|
|
99
|
+
this.close();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var ClientTransport = class {
|
|
103
|
+
child = null;
|
|
104
|
+
buffer = "";
|
|
105
|
+
handlers = /* @__PURE__ */ new Set();
|
|
106
|
+
closed = false;
|
|
107
|
+
resolveRead = null;
|
|
108
|
+
messageQueue = [];
|
|
109
|
+
opts;
|
|
110
|
+
constructor(options) {
|
|
111
|
+
this.opts = {
|
|
112
|
+
handshakeTimeoutMs: 3e4,
|
|
113
|
+
...options
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async start() {
|
|
117
|
+
if (this.child) return;
|
|
118
|
+
const [{ spawn: spawn2 }, { buildChildEnv }, os] = await Promise.all([
|
|
119
|
+
import('child_process'),
|
|
120
|
+
import('@wrongstack/core'),
|
|
121
|
+
import('os')
|
|
122
|
+
]);
|
|
123
|
+
return new Promise((resolve3, reject) => {
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
reject(
|
|
126
|
+
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
127
|
+
);
|
|
128
|
+
}, this.opts.handshakeTimeoutMs);
|
|
129
|
+
const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
|
|
130
|
+
const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
|
|
131
|
+
try {
|
|
132
|
+
this.child = spawn2(this.opts.command, this.opts.args ?? [], {
|
|
133
|
+
env: { ...buildChildEnv(), ...this.opts.env },
|
|
134
|
+
cwd: spawnCwd,
|
|
135
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
136
|
+
windowsHide: true,
|
|
137
|
+
// On Windows, most ACP-supporting tools (claude, gemini, codex,
|
|
138
|
+
// qwen, copilot) are installed as `.cmd` shims under
|
|
139
|
+
// AppData\Roaming\npm\. Node's spawn won't find them via
|
|
140
|
+
// `shell: false` because the .cmd extension is not in the
|
|
141
|
+
// default PATHEXT lookup. The argv here is always from our
|
|
142
|
+
// own static catalog or from a hardcoded spec, never from
|
|
143
|
+
// user input, so shell-expansion is bounded.
|
|
144
|
+
shell: process.platform === "win32"
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
reject(err);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const child = this.child;
|
|
152
|
+
child.stdout.setEncoding("utf8");
|
|
153
|
+
let settled = false;
|
|
154
|
+
const onSpawnFailure = (err) => {
|
|
155
|
+
if (settled) {
|
|
156
|
+
this.closed = true;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
settled = true;
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
reject(err);
|
|
162
|
+
};
|
|
163
|
+
child.on("error", onSpawnFailure);
|
|
164
|
+
child.stdout.on("error", onSpawnFailure);
|
|
165
|
+
if (this.opts.skipHandshakeMarker) {
|
|
166
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
167
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
168
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
169
|
+
child.once("spawn", () => {
|
|
170
|
+
if (settled) return;
|
|
171
|
+
settled = true;
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
resolve3();
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const onReady = () => {
|
|
178
|
+
if (settled) return;
|
|
179
|
+
settled = true;
|
|
180
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
181
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
182
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
183
|
+
clearTimeout(timeout);
|
|
184
|
+
resolve3();
|
|
185
|
+
};
|
|
186
|
+
const waitForMarker = (chunk) => {
|
|
187
|
+
this.buffer += chunk;
|
|
188
|
+
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
189
|
+
if (idx !== -1) {
|
|
190
|
+
this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
191
|
+
child.stdout.removeListener("data", waitForMarker);
|
|
192
|
+
onReady();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
child.stdout.on("data", waitForMarker);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
send(msg) {
|
|
199
|
+
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
200
|
+
return new Promise((resolve3, reject) => {
|
|
201
|
+
const line = JSON.stringify(msg) + "\n";
|
|
202
|
+
this.child?.stdin.write(line, "utf8", (err) => {
|
|
203
|
+
if (err) reject(err);
|
|
204
|
+
else resolve3();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
read() {
|
|
209
|
+
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
210
|
+
if (this.closed) return Promise.resolve(null);
|
|
211
|
+
return new Promise((resolve3) => {
|
|
212
|
+
this.resolveRead = resolve3;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
onMessage(handler) {
|
|
216
|
+
this.handlers.add(handler);
|
|
217
|
+
return () => this.handlers.delete(handler);
|
|
218
|
+
}
|
|
219
|
+
stop() {
|
|
220
|
+
if (!this.child) return;
|
|
221
|
+
this.closed = true;
|
|
222
|
+
try {
|
|
223
|
+
this.child.kill();
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
this.child = null;
|
|
227
|
+
}
|
|
228
|
+
onChildData(chunk) {
|
|
229
|
+
this.buffer += chunk;
|
|
230
|
+
const lines = this.buffer.split("\n");
|
|
231
|
+
this.buffer = lines.pop() ?? "";
|
|
232
|
+
for (const raw of lines) {
|
|
233
|
+
if (!raw.trim()) continue;
|
|
234
|
+
try {
|
|
235
|
+
this.dispatch(JSON.parse(raw));
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
onChildError(chunk) {
|
|
241
|
+
writeErr(`[acp-child stderr] ${chunk}`);
|
|
242
|
+
}
|
|
243
|
+
onChildClose(code) {
|
|
244
|
+
this.closed = true;
|
|
245
|
+
this.resolveRead?.(null);
|
|
246
|
+
this.resolveRead = null;
|
|
247
|
+
if (code !== 0 && code !== null) {
|
|
248
|
+
writeErr(`[acp-child exited with code ${code}]
|
|
249
|
+
`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
dispatch(msg) {
|
|
253
|
+
if (this.resolveRead) {
|
|
254
|
+
const resolve3 = this.resolveRead;
|
|
255
|
+
this.resolveRead = null;
|
|
256
|
+
resolve3(msg);
|
|
257
|
+
} else {
|
|
258
|
+
this.messageQueue.push(msg);
|
|
259
|
+
}
|
|
260
|
+
for (const handler of this.handlers) {
|
|
261
|
+
try {
|
|
262
|
+
handler(msg);
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// src/client/websocket-transport.ts
|
|
270
|
+
var WebSocketClientTransport = class {
|
|
271
|
+
ws = null;
|
|
272
|
+
handlers = /* @__PURE__ */ new Set();
|
|
273
|
+
closed = false;
|
|
274
|
+
opts;
|
|
275
|
+
constructor(opts) {
|
|
276
|
+
this.opts = opts;
|
|
277
|
+
}
|
|
278
|
+
start() {
|
|
279
|
+
const WS = globalThis.WebSocket;
|
|
280
|
+
if (!WS) {
|
|
281
|
+
return Promise.reject(
|
|
282
|
+
new Error(
|
|
283
|
+
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
288
|
+
return new Promise((resolve3, reject) => {
|
|
289
|
+
let settled = false;
|
|
290
|
+
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
291
|
+
this.ws = ws;
|
|
292
|
+
const timer = setTimeout(() => {
|
|
293
|
+
if (settled) return;
|
|
294
|
+
settled = true;
|
|
295
|
+
try {
|
|
296
|
+
ws.close();
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
300
|
+
}, timeoutMs);
|
|
301
|
+
ws.addEventListener("open", () => {
|
|
302
|
+
if (settled) return;
|
|
303
|
+
settled = true;
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
resolve3();
|
|
306
|
+
});
|
|
307
|
+
ws.addEventListener("error", (ev) => {
|
|
308
|
+
if (settled) {
|
|
309
|
+
this.closed = true;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
settled = true;
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
315
|
+
reject(new Error(message));
|
|
316
|
+
});
|
|
317
|
+
ws.addEventListener("close", () => {
|
|
318
|
+
this.closed = true;
|
|
319
|
+
});
|
|
320
|
+
ws.addEventListener("message", (ev) => {
|
|
321
|
+
this.onData(ev.data);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
send(msg) {
|
|
326
|
+
if (this.closed || !this.ws) {
|
|
327
|
+
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
this.ws.send(JSON.stringify(msg));
|
|
331
|
+
return Promise.resolve();
|
|
332
|
+
} catch (err) {
|
|
333
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
onMessage(handler) {
|
|
337
|
+
this.handlers.add(handler);
|
|
338
|
+
return () => this.handlers.delete(handler);
|
|
339
|
+
}
|
|
340
|
+
stop() {
|
|
341
|
+
this.closed = true;
|
|
342
|
+
if (this.ws) {
|
|
343
|
+
try {
|
|
344
|
+
this.ws.close();
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
this.ws = null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
onData(data) {
|
|
351
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
352
|
+
if (!text.trim()) return;
|
|
353
|
+
let msg;
|
|
354
|
+
try {
|
|
355
|
+
msg = JSON.parse(text);
|
|
356
|
+
} catch {
|
|
357
|
+
for (const line of text.split("\n")) {
|
|
358
|
+
if (!line.trim()) continue;
|
|
359
|
+
try {
|
|
360
|
+
this.dispatch(JSON.parse(line));
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.dispatch(msg);
|
|
367
|
+
}
|
|
368
|
+
dispatch(msg) {
|
|
369
|
+
for (const handler of [...this.handlers]) {
|
|
370
|
+
try {
|
|
371
|
+
handler(msg);
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// src/types/acp-v1.ts
|
|
379
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
380
|
+
var FsError = class extends Error {
|
|
381
|
+
code;
|
|
382
|
+
path;
|
|
383
|
+
constructor(code, path4, message) {
|
|
384
|
+
super(message);
|
|
385
|
+
this.name = "FsError";
|
|
386
|
+
this.code = code;
|
|
387
|
+
this.path = path4;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
var FileServer = class {
|
|
391
|
+
root;
|
|
392
|
+
timeoutMs;
|
|
393
|
+
constructor(opts) {
|
|
394
|
+
this.root = path3.resolve(opts.projectRoot);
|
|
395
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
396
|
+
}
|
|
397
|
+
/** Read a text file. Returns the content as a string. */
|
|
398
|
+
async readTextFile(params) {
|
|
399
|
+
const safe = this.resolveInside(params.path);
|
|
400
|
+
const controller = new AbortController();
|
|
401
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
402
|
+
try {
|
|
403
|
+
const content = await fsp2.readFile(safe, {
|
|
404
|
+
encoding: "utf8",
|
|
405
|
+
signal: controller.signal
|
|
406
|
+
});
|
|
407
|
+
return { content };
|
|
408
|
+
} catch (err) {
|
|
409
|
+
if (controller.signal.aborted) {
|
|
410
|
+
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
411
|
+
}
|
|
412
|
+
throw mapFsError(err, safe);
|
|
413
|
+
} finally {
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** Write a text file. Atomic via write-then-rename. */
|
|
418
|
+
async writeTextFile(params) {
|
|
419
|
+
const safe = this.resolveInside(params.path);
|
|
420
|
+
const controller = new AbortController();
|
|
421
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
422
|
+
const tmp = `${safe}.${randomHex(4)}.tmp`;
|
|
423
|
+
try {
|
|
424
|
+
await fsp2.writeFile(tmp, params.content, {
|
|
425
|
+
encoding: "utf8",
|
|
426
|
+
signal: controller.signal
|
|
427
|
+
});
|
|
428
|
+
await fsp2.rename(tmp, safe);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
try {
|
|
431
|
+
await fsp2.unlink(tmp);
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
if (controller.signal.aborted) {
|
|
435
|
+
throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
|
|
436
|
+
}
|
|
437
|
+
throw mapFsError(err, safe);
|
|
438
|
+
} finally {
|
|
439
|
+
clearTimeout(timer);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Resolve a path; throw `FsError('OUTSIDE_ROOT')` if the result is
|
|
444
|
+
* not under the project root. Symlinks are not followed here — we
|
|
445
|
+
* operate on the textual path. A future hardening pass can
|
|
446
|
+
* `fs.realpath` each access to catch symlink escapes.
|
|
447
|
+
*/
|
|
448
|
+
resolveInside(p) {
|
|
449
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
450
|
+
throw new FsError("INVALID_PATH", p, "path is empty or not a string");
|
|
451
|
+
}
|
|
452
|
+
if (!path3.isAbsolute(p)) {
|
|
453
|
+
throw new FsError("INVALID_PATH", p, "path must be absolute (ACP requirement)");
|
|
454
|
+
}
|
|
455
|
+
const resolved = path3.resolve(p);
|
|
456
|
+
const rootWithSep = this.root.endsWith(path3.sep) ? this.root : this.root + path3.sep;
|
|
457
|
+
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
458
|
+
throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
|
|
459
|
+
}
|
|
460
|
+
return resolved;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
function mapFsError(err, p) {
|
|
464
|
+
const code = err?.code;
|
|
465
|
+
if (code === "ENOENT") return new FsError("ENOENT", p, `no such file: ${p}`);
|
|
466
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
467
|
+
return new FsError("EACCES", p, `permission denied: ${p}`);
|
|
468
|
+
}
|
|
469
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
470
|
+
return new FsError("INVALID_PATH", p, msg);
|
|
471
|
+
}
|
|
472
|
+
function randomHex(bytes) {
|
|
473
|
+
let out = "";
|
|
474
|
+
for (let i = 0; i < bytes * 2; i++) {
|
|
475
|
+
out += Math.floor(Math.random() * 16).toString(16);
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/client/permission.ts
|
|
481
|
+
function pickAllow(options) {
|
|
482
|
+
const ranked = [...options].sort((a, b) => {
|
|
483
|
+
const score = (k) => {
|
|
484
|
+
if (k === "allow_once") return 0;
|
|
485
|
+
if (k === "allow_always") return 1;
|
|
486
|
+
if (k === "reject_once") return 2;
|
|
487
|
+
return 3;
|
|
488
|
+
};
|
|
489
|
+
return score(a.kind) - score(b.kind);
|
|
490
|
+
});
|
|
491
|
+
const chosen = ranked[0];
|
|
492
|
+
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
493
|
+
return { outcome: "cancelled" };
|
|
494
|
+
}
|
|
495
|
+
return { outcome: "selected", optionId: chosen.optionId };
|
|
496
|
+
}
|
|
497
|
+
function pickReject(options) {
|
|
498
|
+
const reject = options.find(
|
|
499
|
+
(o) => o.kind === "reject_once" || o.kind === "reject_always"
|
|
500
|
+
);
|
|
501
|
+
return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
|
|
502
|
+
}
|
|
503
|
+
var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
|
|
504
|
+
var defaultPermissionPolicy = async (req) => {
|
|
505
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
506
|
+
return pickAllow(req.options);
|
|
507
|
+
};
|
|
508
|
+
var readOnlyPermissionPolicy = async (req) => {
|
|
509
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
510
|
+
const kind = req.toolCall.kind;
|
|
511
|
+
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
512
|
+
return pickAllow(req.options);
|
|
513
|
+
}
|
|
514
|
+
return pickReject(req.options);
|
|
515
|
+
};
|
|
516
|
+
function makePermissionPolicy(decide) {
|
|
517
|
+
return async (req) => {
|
|
518
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
519
|
+
const allow = await decide(req);
|
|
520
|
+
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
var TerminalServer = class {
|
|
524
|
+
terminals = /* @__PURE__ */ new Map();
|
|
525
|
+
projectRoot;
|
|
526
|
+
commandTimeoutMs;
|
|
527
|
+
outputByteLimit;
|
|
528
|
+
nextId = 1;
|
|
529
|
+
constructor(opts) {
|
|
530
|
+
this.projectRoot = path3.resolve(opts.projectRoot);
|
|
531
|
+
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
532
|
+
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
533
|
+
if (opts.signal) {
|
|
534
|
+
opts.signal.addEventListener("abort", () => this.releaseAll());
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
538
|
+
create(params) {
|
|
539
|
+
const id = `term_${this.nextId++}`;
|
|
540
|
+
const cwd = this.resolveCwd(params.cwd);
|
|
541
|
+
const proc = spawn(params.command, params.args ?? [], {
|
|
542
|
+
cwd,
|
|
543
|
+
env: this.buildEnv(params.env),
|
|
544
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
545
|
+
windowsHide: true
|
|
546
|
+
// shell: false on purpose. The terminal server is invoked with
|
|
547
|
+
// the agent's explicit argv; turning on shell-mode would make
|
|
548
|
+
// the command a single shell-parsed string, which breaks
|
|
549
|
+
// Windows cmd quoting for the common case of running node with
|
|
550
|
+
// `-e "<script>"`. If a future feature needs shell features
|
|
551
|
+
// (pipes, redirects), it should be opt-in per-call, not the
|
|
552
|
+
// default.
|
|
553
|
+
});
|
|
554
|
+
const state = {
|
|
555
|
+
proc,
|
|
556
|
+
cwd,
|
|
557
|
+
command: params.command,
|
|
558
|
+
args: params.args ?? [],
|
|
559
|
+
output: "",
|
|
560
|
+
retainedBytes: 0,
|
|
561
|
+
truncated: false,
|
|
562
|
+
exitStatus: void 0,
|
|
563
|
+
timeoutHandle: null,
|
|
564
|
+
exitPromise: new Promise((resolve3) => {
|
|
565
|
+
proc.on("close", (code, signalName) => {
|
|
566
|
+
if (state.timeoutHandle) {
|
|
567
|
+
clearTimeout(state.timeoutHandle);
|
|
568
|
+
state.timeoutHandle = null;
|
|
569
|
+
}
|
|
570
|
+
const exitStatus = {
|
|
571
|
+
exitCode: typeof code === "number" ? code : null,
|
|
572
|
+
signal: typeof signalName === "string" ? signalName : null
|
|
573
|
+
};
|
|
574
|
+
state.exitStatus = exitStatus;
|
|
575
|
+
resolve3(exitStatus);
|
|
576
|
+
});
|
|
577
|
+
proc.on("error", (err) => {
|
|
578
|
+
if (state.timeoutHandle) {
|
|
579
|
+
clearTimeout(state.timeoutHandle);
|
|
580
|
+
state.timeoutHandle = null;
|
|
581
|
+
}
|
|
582
|
+
const exitStatus = { exitCode: 127, signal: null };
|
|
583
|
+
state.exitStatus = exitStatus;
|
|
584
|
+
state.output += `[spawn error] ${err.message}
|
|
585
|
+
`;
|
|
586
|
+
state.retainedBytes += Buffer.byteLength(state.output, "utf8");
|
|
587
|
+
resolve3(exitStatus);
|
|
588
|
+
});
|
|
589
|
+
})
|
|
590
|
+
};
|
|
591
|
+
const perCallByteLimit = params.outputByteLimit ?? this.outputByteLimit;
|
|
592
|
+
proc.stdout?.setEncoding("utf8");
|
|
593
|
+
proc.stderr?.setEncoding("utf8");
|
|
594
|
+
const onData = (chunk) => {
|
|
595
|
+
state.output += chunk;
|
|
596
|
+
state.retainedBytes = Buffer.byteLength(state.output, "utf8");
|
|
597
|
+
while (state.retainedBytes > perCallByteLimit) {
|
|
598
|
+
const trimmed = state.output.slice(1);
|
|
599
|
+
state.output = trimmed;
|
|
600
|
+
const newBytes = Buffer.byteLength(state.output, "utf8");
|
|
601
|
+
if (newBytes >= state.retainedBytes) {
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
state.retainedBytes = newBytes;
|
|
605
|
+
state.truncated = true;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
proc.stdout?.on("data", onData);
|
|
609
|
+
proc.stderr?.on("data", onData);
|
|
610
|
+
state.timeoutHandle = setTimeout(() => {
|
|
611
|
+
try {
|
|
612
|
+
proc.kill("SIGTERM");
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
}, this.commandTimeoutMs);
|
|
616
|
+
this.terminals.set(id, state);
|
|
617
|
+
return { terminalId: id };
|
|
618
|
+
}
|
|
619
|
+
/** Return captured output and (if available) the exit status. */
|
|
620
|
+
output(terminalId) {
|
|
621
|
+
const state = this.terminals.get(terminalId);
|
|
622
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
623
|
+
return {
|
|
624
|
+
output: state.output,
|
|
625
|
+
truncated: state.truncated,
|
|
626
|
+
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
/** Block until the process exits. Resolves with the exit status. */
|
|
630
|
+
async waitForExit(terminalId) {
|
|
631
|
+
const state = this.terminals.get(terminalId);
|
|
632
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
633
|
+
return state.exitPromise;
|
|
634
|
+
}
|
|
635
|
+
/** Kill the process but keep the terminal record (agent can still read output). */
|
|
636
|
+
kill(terminalId) {
|
|
637
|
+
const state = this.terminals.get(terminalId);
|
|
638
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
639
|
+
try {
|
|
640
|
+
state.proc.kill("SIGTERM");
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/** Kill the process if alive and remove the record. */
|
|
645
|
+
release(terminalId) {
|
|
646
|
+
const state = this.terminals.get(terminalId);
|
|
647
|
+
if (!state) return;
|
|
648
|
+
if (state.timeoutHandle) {
|
|
649
|
+
clearTimeout(state.timeoutHandle);
|
|
650
|
+
state.timeoutHandle = null;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
state.proc.kill("SIGKILL");
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
this.terminals.delete(terminalId);
|
|
657
|
+
}
|
|
658
|
+
/** Kill all active terminals. Used on session close. */
|
|
659
|
+
releaseAll() {
|
|
660
|
+
for (const id of [...this.terminals.keys()]) {
|
|
661
|
+
this.release(id);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
resolveCwd(cwd) {
|
|
665
|
+
if (!cwd) return this.projectRoot;
|
|
666
|
+
const resolved = path3.resolve(cwd);
|
|
667
|
+
const rootWithSep = this.projectRoot.endsWith(path3.sep) ? this.projectRoot : this.projectRoot + path3.sep;
|
|
668
|
+
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
669
|
+
return this.projectRoot;
|
|
670
|
+
}
|
|
671
|
+
return resolved;
|
|
672
|
+
}
|
|
673
|
+
buildEnv(agentEnv) {
|
|
674
|
+
const env = { ...process.env };
|
|
675
|
+
if (process.platform === "win32") {
|
|
676
|
+
if (env.Path !== void 0 && env.PATH === void 0) env.PATH = env.Path;
|
|
677
|
+
if (env.PATHEXT !== void 0 && env.PATHEXT_CASE === void 0) {
|
|
678
|
+
env.PATHEXT_CASE = env.PATHEXT;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (agentEnv) {
|
|
682
|
+
for (const { name, value } of agentEnv) {
|
|
683
|
+
env[name] = value;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return env;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/client/acp-session.ts
|
|
691
|
+
var ACPSessionError = class extends Error {
|
|
692
|
+
kind;
|
|
693
|
+
cause;
|
|
694
|
+
constructor(kind, message, cause) {
|
|
695
|
+
super(message);
|
|
696
|
+
this.name = "ACPSessionError";
|
|
697
|
+
this.kind = kind;
|
|
698
|
+
this.cause = cause;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
function isJsonRpcError(v) {
|
|
702
|
+
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
703
|
+
}
|
|
704
|
+
var ACPSession = class _ACPSession {
|
|
705
|
+
transport;
|
|
706
|
+
fileServer;
|
|
707
|
+
terminalServer;
|
|
708
|
+
permissionPolicy;
|
|
709
|
+
timeoutMs;
|
|
710
|
+
opts;
|
|
711
|
+
state = "init";
|
|
712
|
+
sessionId = null;
|
|
713
|
+
/** Pending outbound requests (initialize, session/new, session/prompt, etc). */
|
|
714
|
+
pending = /* @__PURE__ */ new Map();
|
|
715
|
+
nextId = 1;
|
|
716
|
+
/** True after close() has been called. */
|
|
717
|
+
closed = false;
|
|
718
|
+
// Agent-provided info from the initialize handshake
|
|
719
|
+
agentCapabilities = {};
|
|
720
|
+
agentInfo = null;
|
|
721
|
+
authMethods = [];
|
|
722
|
+
/** Protocol version negotiated with the agent during initialize. */
|
|
723
|
+
negotiatedVersion = ACP_PROTOCOL_VERSION;
|
|
724
|
+
constructor(opts, transport) {
|
|
725
|
+
this.opts = opts;
|
|
726
|
+
this.transport = transport;
|
|
727
|
+
this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
728
|
+
const fsOpts = {
|
|
729
|
+
projectRoot: opts.projectRoot
|
|
730
|
+
};
|
|
731
|
+
if (opts.fsTimeoutMs !== void 0) fsOpts.timeoutMs = opts.fsTimeoutMs;
|
|
732
|
+
this.fileServer = new FileServer(fsOpts);
|
|
733
|
+
const termOpts = {
|
|
734
|
+
projectRoot: opts.projectRoot
|
|
735
|
+
};
|
|
736
|
+
if (opts.terminalTimeoutMs !== void 0) {
|
|
737
|
+
termOpts.commandTimeoutMs = opts.terminalTimeoutMs;
|
|
738
|
+
}
|
|
739
|
+
if (opts.terminalOutputByteLimit !== void 0) {
|
|
740
|
+
termOpts.outputByteLimit = opts.terminalOutputByteLimit;
|
|
741
|
+
}
|
|
742
|
+
this.terminalServer = new TerminalServer(termOpts);
|
|
743
|
+
this.permissionPolicy = opts.permissionPolicy ?? defaultPermissionPolicy;
|
|
744
|
+
}
|
|
745
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
746
|
+
// Public accessors
|
|
747
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
748
|
+
/** Agent capabilities advertised during initialize. */
|
|
749
|
+
getCapabilities() {
|
|
750
|
+
return { ...this.agentCapabilities };
|
|
751
|
+
}
|
|
752
|
+
/** Authentication methods advertised by the agent. */
|
|
753
|
+
getAuthMethods() {
|
|
754
|
+
return [...this.authMethods];
|
|
755
|
+
}
|
|
756
|
+
/** Agent info (name, title, version) from initialize. */
|
|
757
|
+
getAgentInfo() {
|
|
758
|
+
return this.agentInfo;
|
|
759
|
+
}
|
|
760
|
+
/** Whether the agent requires authentication (has auth methods). */
|
|
761
|
+
requiresAuth() {
|
|
762
|
+
return this.authMethods.length > 0;
|
|
763
|
+
}
|
|
764
|
+
/** Current session id, if one exists. */
|
|
765
|
+
getSessionId() {
|
|
766
|
+
return this.sessionId;
|
|
767
|
+
}
|
|
768
|
+
/** Protocol version negotiated during initialize. */
|
|
769
|
+
getNegotiatedVersion() {
|
|
770
|
+
return this.negotiatedVersion;
|
|
771
|
+
}
|
|
772
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
773
|
+
// Lifecycle — start
|
|
774
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
775
|
+
/**
|
|
776
|
+
* Spawn the child, run the initialize handshake, install the
|
|
777
|
+
* message dispatch, and return a ready session.
|
|
778
|
+
*/
|
|
779
|
+
static async start(opts) {
|
|
780
|
+
const transportOpts = {
|
|
781
|
+
command: opts.command,
|
|
782
|
+
args: opts.args ? [...opts.args] : [],
|
|
783
|
+
handshakeTimeoutMs: 3e4,
|
|
784
|
+
skipHandshakeMarker: true
|
|
785
|
+
};
|
|
786
|
+
if (opts.env !== void 0) transportOpts.env = opts.env;
|
|
787
|
+
if (opts.cwd !== void 0) transportOpts.cwd = opts.cwd;
|
|
788
|
+
const transport = new ClientTransport(transportOpts);
|
|
789
|
+
return _ACPSession.attach(opts, transport, `failed to spawn ${opts.command}`);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Connect to a REMOTE ACP agent over a WebSocket instead of spawning a
|
|
793
|
+
* local subprocess. `opts.command` is ignored for the wire (a label is
|
|
794
|
+
* still useful for `role`); everything else (projectRoot sandbox for
|
|
795
|
+
* fs/terminal, timeouts, permission policy, MCP servers) applies the same.
|
|
796
|
+
*/
|
|
797
|
+
static async connectWebSocket(wsOpts, opts) {
|
|
798
|
+
const transport = new WebSocketClientTransport(wsOpts);
|
|
799
|
+
return _ACPSession.attach(opts, transport, `failed to connect to ${wsOpts.url}`);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Connect using a caller-supplied transport. Lets advanced callers plug
|
|
803
|
+
* in their own wire (SDK streams, in-process pipes, test doubles).
|
|
804
|
+
*/
|
|
805
|
+
static async connect(transport, opts) {
|
|
806
|
+
return _ACPSession.attach(opts, transport, "failed to connect transport");
|
|
807
|
+
}
|
|
808
|
+
/** Shared connect path: start the transport, install dispatch, handshake. */
|
|
809
|
+
static async attach(opts, transport, spawnErrLabel) {
|
|
810
|
+
try {
|
|
811
|
+
await transport.start();
|
|
812
|
+
} catch (err) {
|
|
813
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
814
|
+
throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
|
|
815
|
+
}
|
|
816
|
+
const session = new _ACPSession(opts, transport);
|
|
817
|
+
transport.onMessage((msg) => session.handleMessage(msg));
|
|
818
|
+
try {
|
|
819
|
+
await session.initialize();
|
|
820
|
+
} catch (err) {
|
|
821
|
+
try {
|
|
822
|
+
transport.stop();
|
|
823
|
+
} catch {
|
|
824
|
+
}
|
|
825
|
+
throw err;
|
|
826
|
+
}
|
|
827
|
+
return session;
|
|
828
|
+
}
|
|
829
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
830
|
+
// Initialization
|
|
831
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
832
|
+
async initialize() {
|
|
833
|
+
const id = this.allocId();
|
|
834
|
+
const result = await this.sendRequest(id, "initialize", {
|
|
835
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
836
|
+
clientCapabilities: {
|
|
837
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
838
|
+
terminal: true
|
|
839
|
+
},
|
|
840
|
+
clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.263.0" }
|
|
841
|
+
});
|
|
842
|
+
if (isJsonRpcError(result)) {
|
|
843
|
+
throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
|
|
844
|
+
}
|
|
845
|
+
if (typeof result !== "object" || result === null || typeof result.protocolVersion !== "number") {
|
|
846
|
+
throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
|
|
847
|
+
}
|
|
848
|
+
const r = result;
|
|
849
|
+
if (r.protocolVersion > ACP_PROTOCOL_VERSION) {
|
|
850
|
+
throw new ACPSessionError(
|
|
851
|
+
"unsupported_capability",
|
|
852
|
+
`agent requires protocolVersion=${r.protocolVersion}, client supports up to ${ACP_PROTOCOL_VERSION}`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
this.negotiatedVersion = r.protocolVersion;
|
|
856
|
+
this.agentCapabilities = r.agentCapabilities ?? {};
|
|
857
|
+
this.agentInfo = r.agentInfo ?? null;
|
|
858
|
+
this.authMethods = r.authMethods ?? [];
|
|
859
|
+
this.state = "ready";
|
|
860
|
+
}
|
|
861
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
862
|
+
// Authentication
|
|
863
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
864
|
+
/**
|
|
865
|
+
* Authenticate with the agent using one of the advertised auth methods.
|
|
866
|
+
* Call this AFTER start() and BEFORE any session/new call.
|
|
867
|
+
*
|
|
868
|
+
* Throws ACPSessionError('auth_failed') if the agent rejects the
|
|
869
|
+
* authentication or if the methodId is not in the advertised list.
|
|
870
|
+
*/
|
|
871
|
+
async authenticate(methodId) {
|
|
872
|
+
if (this.state === "closed") {
|
|
873
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
874
|
+
}
|
|
875
|
+
if (this.state !== "ready") {
|
|
876
|
+
throw new ACPSessionError(
|
|
877
|
+
"protocol_error",
|
|
878
|
+
`authenticate called in state=${this.state} (expected 'ready')`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
882
|
+
throw new ACPSessionError(
|
|
883
|
+
"auth_failed",
|
|
884
|
+
`auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const id = this.allocId();
|
|
888
|
+
const result = await this.sendRequest(id, "authenticate", { methodId });
|
|
889
|
+
if (isJsonRpcError(result)) {
|
|
890
|
+
throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
|
|
891
|
+
}
|
|
892
|
+
this.state = "authenticated";
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Log out from the current authenticated session.
|
|
896
|
+
* Only callable if the agent advertises `auth.logout` capability.
|
|
897
|
+
*/
|
|
898
|
+
async logout() {
|
|
899
|
+
if (this.state === "closed") {
|
|
900
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
901
|
+
}
|
|
902
|
+
if (!this.agentCapabilities.auth?.logout) {
|
|
903
|
+
throw new ACPSessionError(
|
|
904
|
+
"unsupported_capability",
|
|
905
|
+
"agent does not support logout (auth.logout capability not advertised)"
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
const id = this.allocId();
|
|
909
|
+
const result = await this.sendRequest(id, "logout", {});
|
|
910
|
+
if (isJsonRpcError(result)) {
|
|
911
|
+
throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
|
|
912
|
+
}
|
|
913
|
+
this.state = "ready";
|
|
914
|
+
}
|
|
915
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
916
|
+
// Session management
|
|
917
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
918
|
+
/**
|
|
919
|
+
* Load an existing session. The agent replays the conversation history
|
|
920
|
+
* via session/update notifications before responding.
|
|
921
|
+
*
|
|
922
|
+
* Only works if the agent advertises `loadSession` capability.
|
|
923
|
+
*
|
|
924
|
+
* @param sessionId - The session to load
|
|
925
|
+
* @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
|
|
926
|
+
* @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
|
|
927
|
+
*/
|
|
928
|
+
async loadSession(sessionId, mcpServers, cwd) {
|
|
929
|
+
if (this.closed) {
|
|
930
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
931
|
+
}
|
|
932
|
+
if (!this.agentCapabilities.loadSession) {
|
|
933
|
+
throw new ACPSessionError(
|
|
934
|
+
"unsupported_capability",
|
|
935
|
+
"agent does not support session/load (loadSession capability not advertised)"
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
if (this.sessionId) {
|
|
939
|
+
await this.closeSession();
|
|
940
|
+
}
|
|
941
|
+
this.resetScratch();
|
|
942
|
+
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
943
|
+
const id = this.allocId();
|
|
944
|
+
const result = await this.sendRequest(id, "session/load", {
|
|
945
|
+
sessionId,
|
|
946
|
+
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
947
|
+
mcpServers: servers
|
|
948
|
+
});
|
|
949
|
+
if (isJsonRpcError(result)) {
|
|
950
|
+
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
951
|
+
}
|
|
952
|
+
this.sessionId = sessionId;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Resume an existing session without replaying history.
|
|
956
|
+
*
|
|
957
|
+
* Only works if the agent advertises `sessionCapabilities.resume`.
|
|
958
|
+
*
|
|
959
|
+
* @param sessionId - The session to resume
|
|
960
|
+
* @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
|
|
961
|
+
* @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
|
|
962
|
+
*/
|
|
963
|
+
async resumeSession(sessionId, mcpServers, cwd) {
|
|
964
|
+
if (this.closed) {
|
|
965
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
966
|
+
}
|
|
967
|
+
if (!this.agentCapabilities.sessionCapabilities?.resume) {
|
|
968
|
+
throw new ACPSessionError(
|
|
969
|
+
"unsupported_capability",
|
|
970
|
+
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
if (this.sessionId) {
|
|
974
|
+
await this.closeSession();
|
|
975
|
+
}
|
|
976
|
+
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
977
|
+
const id = this.allocId();
|
|
978
|
+
const result = await this.sendRequest(id, "session/resume", {
|
|
979
|
+
sessionId,
|
|
980
|
+
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
981
|
+
mcpServers: servers
|
|
982
|
+
});
|
|
983
|
+
if (isJsonRpcError(result)) {
|
|
984
|
+
throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
|
|
985
|
+
}
|
|
986
|
+
this.sessionId = sessionId;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* List existing sessions known to the agent.
|
|
990
|
+
*
|
|
991
|
+
* Only works if the agent advertises `sessionCapabilities.list`.
|
|
992
|
+
*/
|
|
993
|
+
async listSessions(cursor, cwd) {
|
|
994
|
+
if (this.closed) {
|
|
995
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
996
|
+
}
|
|
997
|
+
if (!this.agentCapabilities.sessionCapabilities?.list) {
|
|
998
|
+
throw new ACPSessionError(
|
|
999
|
+
"unsupported_capability",
|
|
1000
|
+
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
const id = this.allocId();
|
|
1004
|
+
const params = {};
|
|
1005
|
+
if (cursor !== void 0) params.cursor = cursor;
|
|
1006
|
+
if (cwd !== void 0) params.cwd = cwd;
|
|
1007
|
+
const result = await this.sendRequest(id, "session/list", params);
|
|
1008
|
+
if (isJsonRpcError(result)) {
|
|
1009
|
+
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
1010
|
+
}
|
|
1011
|
+
const r = result;
|
|
1012
|
+
return {
|
|
1013
|
+
sessions: r.sessions ?? [],
|
|
1014
|
+
nextCursor: r.nextCursor
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Delete a session from the agent's session list.
|
|
1019
|
+
*
|
|
1020
|
+
* Only works if the agent advertises `sessionCapabilities.delete`.
|
|
1021
|
+
*/
|
|
1022
|
+
async deleteSession(sessionId) {
|
|
1023
|
+
if (this.closed) {
|
|
1024
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
1025
|
+
}
|
|
1026
|
+
if (!this.agentCapabilities.sessionCapabilities?.delete) {
|
|
1027
|
+
throw new ACPSessionError(
|
|
1028
|
+
"unsupported_capability",
|
|
1029
|
+
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
const id = this.allocId();
|
|
1033
|
+
const result = await this.sendRequest(id, "session/delete", { sessionId });
|
|
1034
|
+
if (isJsonRpcError(result)) {
|
|
1035
|
+
throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
|
|
1036
|
+
}
|
|
1037
|
+
if (this.sessionId === sessionId) {
|
|
1038
|
+
this.sessionId = null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Fork a session — create a new session from an existing one.
|
|
1043
|
+
*/
|
|
1044
|
+
async forkSession(sourceSessionId, cwd, mcpServers) {
|
|
1045
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1046
|
+
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
1047
|
+
const id = this.allocId();
|
|
1048
|
+
const result = await this.sendRequest(id, "session/fork", {
|
|
1049
|
+
sessionId: sourceSessionId,
|
|
1050
|
+
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
1051
|
+
...servers.length > 0 ? { mcpServers: servers } : {}
|
|
1052
|
+
});
|
|
1053
|
+
if (isJsonRpcError(result)) {
|
|
1054
|
+
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
1055
|
+
}
|
|
1056
|
+
const newId = result.sessionId;
|
|
1057
|
+
if (typeof newId !== "string" || !newId) {
|
|
1058
|
+
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
1059
|
+
}
|
|
1060
|
+
return newId;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Set the active mode for a session.
|
|
1064
|
+
*/
|
|
1065
|
+
async setMode(sessionId, modeId) {
|
|
1066
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1067
|
+
const id = this.allocId();
|
|
1068
|
+
const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
1069
|
+
if (isJsonRpcError(result)) {
|
|
1070
|
+
throw new ACPSessionError("prompt_failed", `session/set_mode failed: ${result.message}`, result);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Set a configuration option for a session.
|
|
1075
|
+
*/
|
|
1076
|
+
async setConfigOption(sessionId, configId, value) {
|
|
1077
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1078
|
+
const id = this.allocId();
|
|
1079
|
+
const result = await this.sendRequest(id, "session/set_config_option", {
|
|
1080
|
+
sessionId,
|
|
1081
|
+
configId,
|
|
1082
|
+
value
|
|
1083
|
+
});
|
|
1084
|
+
if (isJsonRpcError(result)) {
|
|
1085
|
+
throw new ACPSessionError("prompt_failed", `session/set_config_option failed: ${result.message}`, result);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* List available providers and the current provider.
|
|
1090
|
+
*/
|
|
1091
|
+
async listProviders() {
|
|
1092
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1093
|
+
const id = this.allocId();
|
|
1094
|
+
const result = await this.sendRequest(id, "providers/list", {});
|
|
1095
|
+
if (isJsonRpcError(result)) {
|
|
1096
|
+
throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
|
|
1097
|
+
}
|
|
1098
|
+
const r = result;
|
|
1099
|
+
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Send an MCP message to the agent for routing.
|
|
1103
|
+
*/
|
|
1104
|
+
async mcpMessage(connectionId, message) {
|
|
1105
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1106
|
+
const id = this.allocId();
|
|
1107
|
+
const result = await this.sendRequest(id, "mcp/message", { connectionId, message });
|
|
1108
|
+
if (isJsonRpcError(result)) {
|
|
1109
|
+
throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
|
|
1110
|
+
}
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Set the active provider for the agent.
|
|
1115
|
+
*/
|
|
1116
|
+
async setProvider(providerId, config) {
|
|
1117
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1118
|
+
const id = this.allocId();
|
|
1119
|
+
const result = await this.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
1120
|
+
if (isJsonRpcError(result)) {
|
|
1121
|
+
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Disable the current provider.
|
|
1126
|
+
*/
|
|
1127
|
+
async disableProvider() {
|
|
1128
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1129
|
+
const id = this.allocId();
|
|
1130
|
+
const result = await this.sendRequest(id, "providers/disable", {});
|
|
1131
|
+
if (isJsonRpcError(result)) {
|
|
1132
|
+
throw new ACPSessionError("prompt_failed", `providers/disable failed: ${result.message}`, result);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1136
|
+
// Prompt
|
|
1137
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1138
|
+
/**
|
|
1139
|
+
* Run one prompt turn. Creates a session if needed, sends the
|
|
1140
|
+
* prompt, streams session/update notifications, and resolves with
|
|
1141
|
+
* the agent's response.
|
|
1142
|
+
*
|
|
1143
|
+
* @param blocks - Content blocks to send. Use `textContent()` for plain
|
|
1144
|
+
* text, or include ImageContent/AudioContent if the agent's
|
|
1145
|
+
* `promptCapabilities` allow it.
|
|
1146
|
+
* @param signal - AbortSignal for cancellation.
|
|
1147
|
+
*
|
|
1148
|
+
* Cancellation: if `signal` aborts mid-prompt, we send
|
|
1149
|
+
* `session/cancel` (a notification per spec) and keep accepting
|
|
1150
|
+
* updates until the agent returns with `stopReason: 'cancelled'`.
|
|
1151
|
+
* The result is the same shape as a normal turn, with
|
|
1152
|
+
* `stopReason === 'cancelled'`.
|
|
1153
|
+
*/
|
|
1154
|
+
async prompt(blocks, signal, onProgress) {
|
|
1155
|
+
if (this.closed) {
|
|
1156
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
1157
|
+
}
|
|
1158
|
+
if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
|
|
1159
|
+
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (signal.aborted) {
|
|
1162
|
+
return emptyRunResult("cancelled");
|
|
1163
|
+
}
|
|
1164
|
+
if (!this.sessionId) {
|
|
1165
|
+
await this.createSession();
|
|
1166
|
+
}
|
|
1167
|
+
this.resetScratch();
|
|
1168
|
+
this.progressHandler = onProgress ?? null;
|
|
1169
|
+
const promptId = this.allocId();
|
|
1170
|
+
const turnPromise = this.sendRequest(
|
|
1171
|
+
promptId,
|
|
1172
|
+
"session/prompt",
|
|
1173
|
+
{
|
|
1174
|
+
sessionId: this.sessionId,
|
|
1175
|
+
prompt: blocks
|
|
1176
|
+
},
|
|
1177
|
+
this.timeoutMs
|
|
1178
|
+
);
|
|
1179
|
+
let cancelled = false;
|
|
1180
|
+
const onAbort = () => {
|
|
1181
|
+
cancelled = true;
|
|
1182
|
+
this.transport.send({
|
|
1183
|
+
jsonrpc: "2.0",
|
|
1184
|
+
method: "session/cancel",
|
|
1185
|
+
params: { sessionId: this.sessionId }
|
|
1186
|
+
}).catch(() => {
|
|
1187
|
+
});
|
|
1188
|
+
};
|
|
1189
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1190
|
+
this.state = "prompting";
|
|
1191
|
+
let response;
|
|
1192
|
+
try {
|
|
1193
|
+
response = await turnPromise;
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
this.state = "done";
|
|
1196
|
+
signal.removeEventListener("abort", onAbort);
|
|
1197
|
+
if (cancelled || signal.aborted) {
|
|
1198
|
+
throw new ACPSessionError("aborted", "prompt was aborted by the parent");
|
|
1199
|
+
}
|
|
1200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1201
|
+
throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
|
|
1202
|
+
} finally {
|
|
1203
|
+
signal.removeEventListener("abort", onAbort);
|
|
1204
|
+
this.progressHandler = null;
|
|
1205
|
+
}
|
|
1206
|
+
this.state = "done";
|
|
1207
|
+
if (isJsonRpcError(response)) {
|
|
1208
|
+
throw new ACPSessionError("prompt_failed", `agent error: ${response.message}`, response);
|
|
1209
|
+
}
|
|
1210
|
+
const stopReason = response.stopReason ?? "end_turn";
|
|
1211
|
+
const finalText = this.scratch.text;
|
|
1212
|
+
return {
|
|
1213
|
+
text: finalText,
|
|
1214
|
+
stopReason,
|
|
1215
|
+
hasText: finalText.length > 0,
|
|
1216
|
+
usage: this.scratch.usage,
|
|
1217
|
+
plan: this.scratch.plan,
|
|
1218
|
+
toolCalls: [...this.scratch.toolCalls.values()],
|
|
1219
|
+
diffs: this.scratch.diffs,
|
|
1220
|
+
thoughts: this.scratch.thoughts
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
async createSession() {
|
|
1224
|
+
const servers = this.filterMcpServers(this.opts.mcpServers);
|
|
1225
|
+
const id = this.allocId();
|
|
1226
|
+
const result = await this.sendRequest(id, "session/new", {
|
|
1227
|
+
cwd: this.opts.cwd ?? this.opts.projectRoot,
|
|
1228
|
+
mcpServers: servers
|
|
1229
|
+
});
|
|
1230
|
+
if (isJsonRpcError(result)) {
|
|
1231
|
+
throw new ACPSessionError(
|
|
1232
|
+
"session_create_failed",
|
|
1233
|
+
`session/new failed: ${result.message}`,
|
|
1234
|
+
result
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const sessionId = result.sessionId;
|
|
1238
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
1239
|
+
throw new ACPSessionError(
|
|
1240
|
+
"protocol_error",
|
|
1241
|
+
"session/new returned no sessionId",
|
|
1242
|
+
result
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
this.sessionId = sessionId;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Close the current session gracefully (if the agent supports it).
|
|
1249
|
+
*
|
|
1250
|
+
* Sends `session/close` JSON-RPC request, then clears the local
|
|
1251
|
+
* session id. Best-effort — errors are swallowed so the caller can
|
|
1252
|
+
* always proceed to transport teardown.
|
|
1253
|
+
*/
|
|
1254
|
+
async closeSession() {
|
|
1255
|
+
if (!this.sessionId) return;
|
|
1256
|
+
const sid = this.sessionId;
|
|
1257
|
+
this.sessionId = null;
|
|
1258
|
+
if (this.agentCapabilities.sessionCapabilities?.close) {
|
|
1259
|
+
const id = this.allocId();
|
|
1260
|
+
try {
|
|
1261
|
+
await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
|
|
1262
|
+
} catch {
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1267
|
+
// Lifecycle — close
|
|
1268
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1269
|
+
/** Tear down the session and kill the child process. */
|
|
1270
|
+
async close() {
|
|
1271
|
+
if (this.closed) return;
|
|
1272
|
+
this.closed = true;
|
|
1273
|
+
this.state = "closed";
|
|
1274
|
+
this.terminalServer.releaseAll();
|
|
1275
|
+
if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
|
|
1276
|
+
try {
|
|
1277
|
+
await this.closeSession();
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
for (const [, p] of this.pending) {
|
|
1282
|
+
clearTimeout(p.timeoutHandle);
|
|
1283
|
+
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
1284
|
+
}
|
|
1285
|
+
this.pending.clear();
|
|
1286
|
+
try {
|
|
1287
|
+
this.transport.stop();
|
|
1288
|
+
} catch {
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1292
|
+
// Helpers
|
|
1293
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1294
|
+
/**
|
|
1295
|
+
* Filter MCP servers according to agent capabilities.
|
|
1296
|
+
* - Stdio servers are always included.
|
|
1297
|
+
* - HTTP servers are only included if agent supports mcpCapabilities.http.
|
|
1298
|
+
* - SSE servers are only included if agent supports mcpCapabilities.sse.
|
|
1299
|
+
*/
|
|
1300
|
+
filterMcpServers(servers) {
|
|
1301
|
+
if (!servers || servers.length === 0) return [];
|
|
1302
|
+
const mcpCaps = this.agentCapabilities.mcpCapabilities ?? {};
|
|
1303
|
+
return servers.filter((s) => {
|
|
1304
|
+
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
1305
|
+
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
1306
|
+
return true;
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1310
|
+
// Wire layer
|
|
1311
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1312
|
+
allocId() {
|
|
1313
|
+
return this.nextId++;
|
|
1314
|
+
}
|
|
1315
|
+
async sendRequest(id, method, params, timeoutMs) {
|
|
1316
|
+
return new Promise((resolve3, reject) => {
|
|
1317
|
+
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
1318
|
+
const handle = setTimeout(() => {
|
|
1319
|
+
this.pending.delete(id);
|
|
1320
|
+
reject(
|
|
1321
|
+
new ACPSessionError(
|
|
1322
|
+
"protocol_error",
|
|
1323
|
+
`${method} timed out after ${effectiveTimeout}ms`
|
|
1324
|
+
)
|
|
1325
|
+
);
|
|
1326
|
+
}, effectiveTimeout);
|
|
1327
|
+
this.pending.set(id, {
|
|
1328
|
+
method,
|
|
1329
|
+
resolve: resolve3,
|
|
1330
|
+
reject,
|
|
1331
|
+
timeoutMs: effectiveTimeout,
|
|
1332
|
+
timeoutHandle: handle
|
|
1333
|
+
});
|
|
1334
|
+
this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
|
1335
|
+
clearTimeout(handle);
|
|
1336
|
+
this.pending.delete(id);
|
|
1337
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1338
|
+
reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
|
|
1339
|
+
});
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Send a JSON-RPC 2.0 success response to an agent-initiated request.
|
|
1344
|
+
*
|
|
1345
|
+
* Per JSON-RPC 2.0 (and the official ACP SDK's message router) a Response
|
|
1346
|
+
* object MUST carry `jsonrpc: "2.0"` and MUST NOT carry a `method` field —
|
|
1347
|
+
* the SDK classifies any object with a `method` key as a Request and drops
|
|
1348
|
+
* it as a response, so an agent's `fs/*`, `terminal/*`, or
|
|
1349
|
+
* `session/request_permission` callback would hang forever. The legacy
|
|
1350
|
+
* `ACPMessage` type predates v1 (requires `method`, lacks `jsonrpc`), so we
|
|
1351
|
+
* build the correct wire object and cast at the boundary.
|
|
1352
|
+
*/
|
|
1353
|
+
sendResult(id, result) {
|
|
1354
|
+
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
1355
|
+
}
|
|
1356
|
+
/** Send a JSON-RPC 2.0 error response (no `method` field, per spec). */
|
|
1357
|
+
sendErrorResponse(id, code, message) {
|
|
1358
|
+
return this.transport.send({
|
|
1359
|
+
jsonrpc: "2.0",
|
|
1360
|
+
id,
|
|
1361
|
+
error: { code, message }
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
handleMessage(msg) {
|
|
1365
|
+
if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
|
|
1366
|
+
const pending = this.pending.get(msg.id);
|
|
1367
|
+
if (!pending) return;
|
|
1368
|
+
clearTimeout(pending.timeoutHandle);
|
|
1369
|
+
this.pending.delete(msg.id);
|
|
1370
|
+
if (msg.error !== void 0) {
|
|
1371
|
+
pending.reject(new Error(msg.error.message ?? "unknown JSON-RPC error"));
|
|
1372
|
+
} else {
|
|
1373
|
+
pending.resolve(msg.result);
|
|
1374
|
+
}
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
if (msg.method === "session/update") {
|
|
1378
|
+
this.handleUpdate(msg);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (msg.method === "session/request_permission") {
|
|
1382
|
+
void this.handlePermissionRequest(msg);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
|
|
1386
|
+
void this.handleFsRequest(msg);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (msg.method?.startsWith("terminal/")) {
|
|
1390
|
+
void this.handleTerminalRequest(msg);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
|
|
1394
|
+
if (msg.id !== void 0) {
|
|
1395
|
+
this.sendResult(msg.id, {}).catch(() => {
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
|
|
1401
|
+
if (msg.id !== void 0) {
|
|
1402
|
+
this.sendResult(msg.id, {}).catch(() => {
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (msg.method === "$/cancel_request") {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (msg.method) {
|
|
1411
|
+
console.warn(`[acp-session] unhandled method: ${msg.method}`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
handleUpdate(msg) {
|
|
1415
|
+
const update = msg.params?.update;
|
|
1416
|
+
if (typeof update !== "object" || update === null) return;
|
|
1417
|
+
const u = update;
|
|
1418
|
+
this.emitProgress({ type: "raw", update: u });
|
|
1419
|
+
switch (u.sessionUpdate) {
|
|
1420
|
+
case "agent_message_chunk": {
|
|
1421
|
+
const text = extractText(u.content);
|
|
1422
|
+
if (text) {
|
|
1423
|
+
this.scratch.text += text;
|
|
1424
|
+
this.emitProgress({ type: "message", text });
|
|
1425
|
+
}
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
case "thought_chunk": {
|
|
1429
|
+
const text = extractText(u.content);
|
|
1430
|
+
if (text) {
|
|
1431
|
+
this.scratch.thoughts += text;
|
|
1432
|
+
this.emitProgress({ type: "thought", text });
|
|
1433
|
+
}
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
case "tool_call":
|
|
1437
|
+
case "tool_call_update": {
|
|
1438
|
+
this.captureToolCall(u, u.sessionUpdate === "tool_call");
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
case "plan":
|
|
1442
|
+
if (Array.isArray(u.entries)) {
|
|
1443
|
+
this.scratch.plan = u.entries;
|
|
1444
|
+
this.emitProgress({ type: "plan", entries: u.entries });
|
|
1445
|
+
}
|
|
1446
|
+
return;
|
|
1447
|
+
case "usage_update":
|
|
1448
|
+
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
1449
|
+
const usage = {
|
|
1450
|
+
used: u.used,
|
|
1451
|
+
size: u.size,
|
|
1452
|
+
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
1453
|
+
};
|
|
1454
|
+
this.scratch.usage = usage;
|
|
1455
|
+
this.emitProgress({ type: "usage", usage });
|
|
1456
|
+
}
|
|
1457
|
+
return;
|
|
1458
|
+
case "available_commands_update":
|
|
1459
|
+
case "current_mode_update":
|
|
1460
|
+
case "config_option_update":
|
|
1461
|
+
case "session_info_update":
|
|
1462
|
+
case "user_message_chunk":
|
|
1463
|
+
case "next_edit_suggestions":
|
|
1464
|
+
case "elicitation":
|
|
1465
|
+
return;
|
|
1466
|
+
default:
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Fold a `tool_call` / `tool_call_update` notification into the scratch
|
|
1472
|
+
* tool-call map (deduped by toolCallId), extract any `diff` content into
|
|
1473
|
+
* the diffs list, and emit live progress.
|
|
1474
|
+
*/
|
|
1475
|
+
captureToolCall(u, isNew) {
|
|
1476
|
+
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
|
|
1477
|
+
if (!toolCallId) return;
|
|
1478
|
+
const prev = this.scratch.toolCalls.get(toolCallId);
|
|
1479
|
+
const record = {
|
|
1480
|
+
toolCallId,
|
|
1481
|
+
title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
|
|
1482
|
+
kind: typeof u.kind === "string" ? u.kind : prev?.kind,
|
|
1483
|
+
status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
|
|
1484
|
+
rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
|
|
1485
|
+
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
|
|
1486
|
+
};
|
|
1487
|
+
this.scratch.toolCalls.set(toolCallId, record);
|
|
1488
|
+
if (Array.isArray(u.content)) {
|
|
1489
|
+
for (const c of u.content) {
|
|
1490
|
+
if (c && typeof c === "object" && c.type === "diff") {
|
|
1491
|
+
const diff = {
|
|
1492
|
+
path: c.path,
|
|
1493
|
+
oldText: c.oldText,
|
|
1494
|
+
newText: c.newText
|
|
1495
|
+
};
|
|
1496
|
+
this.scratch.diffs.push(diff);
|
|
1497
|
+
this.emitProgress({ type: "diff", diff });
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
this.emitProgress({
|
|
1502
|
+
type: isNew ? "tool_call" : "tool_call_update",
|
|
1503
|
+
toolCall: record
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
emitProgress(event) {
|
|
1507
|
+
if (!this.progressHandler) return;
|
|
1508
|
+
try {
|
|
1509
|
+
this.progressHandler(event);
|
|
1510
|
+
} catch {
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/** Live progress handler installed for the duration of a `prompt()` turn. */
|
|
1514
|
+
progressHandler = null;
|
|
1515
|
+
// Per-prompt scratch state
|
|
1516
|
+
scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
1517
|
+
resetScratch() {
|
|
1518
|
+
this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
1519
|
+
}
|
|
1520
|
+
async handlePermissionRequest(msg) {
|
|
1521
|
+
const id = msg.id;
|
|
1522
|
+
if (id === void 0) return;
|
|
1523
|
+
const params = msg.params;
|
|
1524
|
+
const toolCall = params?.toolCall;
|
|
1525
|
+
const options = Array.isArray(params?.options) ? params.options : [];
|
|
1526
|
+
if (!toolCall) {
|
|
1527
|
+
await this.sendErrorResponse(id, -32602, "toolCall is required");
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const policyAbort = new AbortController();
|
|
1531
|
+
const outcome = await this.permissionPolicy({
|
|
1532
|
+
toolCall,
|
|
1533
|
+
options,
|
|
1534
|
+
signal: policyAbort.signal
|
|
1535
|
+
});
|
|
1536
|
+
await this.sendResult(id, { outcome });
|
|
1537
|
+
}
|
|
1538
|
+
async handleFsRequest(msg) {
|
|
1539
|
+
const id = msg.id;
|
|
1540
|
+
if (id === void 0) return;
|
|
1541
|
+
const params = msg.params;
|
|
1542
|
+
if (!params?.path) {
|
|
1543
|
+
await this.sendErrorResponse(id, -32602, "path is required");
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
try {
|
|
1547
|
+
if (msg.method === "fs/read_text_file") {
|
|
1548
|
+
const result = await this.fileServer.readTextFile({
|
|
1549
|
+
sessionId: params.sessionId ?? "",
|
|
1550
|
+
path: params.path
|
|
1551
|
+
});
|
|
1552
|
+
await this.sendResult(id, result);
|
|
1553
|
+
} else {
|
|
1554
|
+
await this.fileServer.writeTextFile({
|
|
1555
|
+
sessionId: params.sessionId ?? "",
|
|
1556
|
+
path: params.path,
|
|
1557
|
+
content: params.content ?? ""
|
|
1558
|
+
});
|
|
1559
|
+
await this.sendResult(id, {});
|
|
1560
|
+
}
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
const code = err instanceof FsError ? -32602 : -32603;
|
|
1563
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1564
|
+
await this.sendErrorResponse(id, code, message);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
async handleTerminalRequest(msg) {
|
|
1568
|
+
const id = msg.id;
|
|
1569
|
+
if (id === void 0) return;
|
|
1570
|
+
const params = msg.params ?? {};
|
|
1571
|
+
try {
|
|
1572
|
+
switch (msg.method) {
|
|
1573
|
+
case "terminal/create": {
|
|
1574
|
+
const createOpts = {
|
|
1575
|
+
sessionId: String(params.sessionId ?? ""),
|
|
1576
|
+
command: String(params.command ?? ""),
|
|
1577
|
+
args: Array.isArray(params.args) ? params.args : []
|
|
1578
|
+
};
|
|
1579
|
+
if (Array.isArray(params.env)) {
|
|
1580
|
+
createOpts.env = params.env;
|
|
1581
|
+
}
|
|
1582
|
+
if (typeof params.cwd === "string") {
|
|
1583
|
+
createOpts.cwd = params.cwd;
|
|
1584
|
+
}
|
|
1585
|
+
if (typeof params.outputByteLimit === "number") {
|
|
1586
|
+
createOpts.outputByteLimit = params.outputByteLimit;
|
|
1587
|
+
}
|
|
1588
|
+
const result = this.terminalServer.create(createOpts);
|
|
1589
|
+
await this.sendResult(id, result);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
case "terminal/output": {
|
|
1593
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1594
|
+
const out = this.terminalServer.output(terminalId);
|
|
1595
|
+
await this.sendResult(id, out);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
case "terminal/wait_for_exit": {
|
|
1599
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1600
|
+
const exit = await this.terminalServer.waitForExit(terminalId);
|
|
1601
|
+
await this.sendResult(id, exit);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
case "terminal/kill": {
|
|
1605
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1606
|
+
this.terminalServer.kill(terminalId);
|
|
1607
|
+
await this.sendResult(id, {});
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
case "terminal/release": {
|
|
1611
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1612
|
+
this.terminalServer.release(terminalId);
|
|
1613
|
+
await this.sendResult(id, {});
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
default:
|
|
1617
|
+
await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
1618
|
+
}
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1621
|
+
await this.sendErrorResponse(id, -32603, message);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
function textContent(text) {
|
|
1626
|
+
return { type: "text", text };
|
|
1627
|
+
}
|
|
1628
|
+
function imageContent(mimeType, data) {
|
|
1629
|
+
return { type: "image", mimeType, data };
|
|
1630
|
+
}
|
|
1631
|
+
function audioContent(mimeType, data) {
|
|
1632
|
+
return { type: "audio", mimeType, data };
|
|
1633
|
+
}
|
|
1634
|
+
function extractText(block) {
|
|
1635
|
+
if (typeof block !== "object" || block === null) return "";
|
|
1636
|
+
const b = block;
|
|
1637
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
1638
|
+
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
1639
|
+
return b.resource.text;
|
|
1640
|
+
}
|
|
1641
|
+
return "";
|
|
1642
|
+
}
|
|
1643
|
+
function isRecord(v) {
|
|
1644
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1645
|
+
}
|
|
1646
|
+
function emptyRunResult(stopReason) {
|
|
1647
|
+
return {
|
|
1648
|
+
text: "",
|
|
1649
|
+
stopReason,
|
|
1650
|
+
hasText: false,
|
|
1651
|
+
toolCalls: [],
|
|
1652
|
+
diffs: [],
|
|
1653
|
+
thoughts: ""
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/agent/protocol-handler.ts
|
|
1658
|
+
function toWire(msg) {
|
|
1659
|
+
return msg;
|
|
1660
|
+
}
|
|
1661
|
+
var WRONGSTACK_VERSION = "0.274.1";
|
|
1662
|
+
var WRONGSTACK_AUTH_METHODS = [
|
|
1663
|
+
{
|
|
1664
|
+
id: "wrongstack-auth",
|
|
1665
|
+
name: "Run wstack auth",
|
|
1666
|
+
description: "Configure a WrongStack model provider in an interactive terminal.",
|
|
1667
|
+
type: "terminal",
|
|
1668
|
+
args: ["auth"]
|
|
1669
|
+
}
|
|
1670
|
+
];
|
|
1671
|
+
var DEFAULT_MODE_ID = "code";
|
|
1672
|
+
var DEFAULT_MODES = [
|
|
1673
|
+
{
|
|
1674
|
+
id: DEFAULT_MODE_ID,
|
|
1675
|
+
name: "Code",
|
|
1676
|
+
description: "Default agent mode for code-generation tasks."
|
|
1677
|
+
}
|
|
1678
|
+
];
|
|
1679
|
+
var ACPProtocolHandler = class {
|
|
1680
|
+
transport;
|
|
1681
|
+
defaultCwd;
|
|
1682
|
+
runTurn;
|
|
1683
|
+
onSessionNew;
|
|
1684
|
+
modes;
|
|
1685
|
+
configOptions;
|
|
1686
|
+
agentName;
|
|
1687
|
+
replayFor;
|
|
1688
|
+
seedFor;
|
|
1689
|
+
store;
|
|
1690
|
+
initialized = false;
|
|
1691
|
+
clientCapabilities = {};
|
|
1692
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1693
|
+
nextId = 1;
|
|
1694
|
+
// Outbound request correlation (server → client requests, e.g.
|
|
1695
|
+
// session/request_permission). Keyed by our own `srv_N` ids.
|
|
1696
|
+
pendingOut = /* @__PURE__ */ new Map();
|
|
1697
|
+
nextOutId = 1;
|
|
1698
|
+
constructor(opts) {
|
|
1699
|
+
this.transport = opts.transport;
|
|
1700
|
+
this.defaultCwd = opts.defaultCwd;
|
|
1701
|
+
this.runTurn = opts.runTurn;
|
|
1702
|
+
this.onSessionNew = opts.onSessionNew ?? (() => {
|
|
1703
|
+
});
|
|
1704
|
+
this.modes = opts.modes ?? DEFAULT_MODES;
|
|
1705
|
+
this.configOptions = opts.configOptions ?? [];
|
|
1706
|
+
this.agentName = opts.agentName ?? "wrongstack";
|
|
1707
|
+
this.replayFor = opts.replayFor;
|
|
1708
|
+
this.seedFor = opts.seedFor;
|
|
1709
|
+
this.store = opts.store;
|
|
1710
|
+
if (typeof this.transport.onMessage === "function") {
|
|
1711
|
+
this.transport.onMessage((m) => this.maybeResolvePending(m));
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Send a request to the client and await its response. Used for
|
|
1716
|
+
* server-initiated calls like `session/request_permission`. Rejects on
|
|
1717
|
+
* timeout or transport error so the caller can pick a safe fallback.
|
|
1718
|
+
*/
|
|
1719
|
+
request(method, params, timeoutMs = 6e4) {
|
|
1720
|
+
const id = `srv_${this.nextOutId++}`;
|
|
1721
|
+
return new Promise((resolve3, reject) => {
|
|
1722
|
+
const timer = setTimeout(() => {
|
|
1723
|
+
this.pendingOut.delete(id);
|
|
1724
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
1725
|
+
}, timeoutMs);
|
|
1726
|
+
this.pendingOut.set(id, { resolve: resolve3, reject, timer });
|
|
1727
|
+
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
1728
|
+
clearTimeout(timer);
|
|
1729
|
+
this.pendingOut.delete(id);
|
|
1730
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
1731
|
+
});
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
maybeResolvePending(m) {
|
|
1735
|
+
const id = m.id;
|
|
1736
|
+
if (typeof id !== "string") return;
|
|
1737
|
+
const pending = this.pendingOut.get(id);
|
|
1738
|
+
if (!pending) return;
|
|
1739
|
+
this.pendingOut.delete(id);
|
|
1740
|
+
clearTimeout(pending.timer);
|
|
1741
|
+
const err = m.error;
|
|
1742
|
+
if (err) pending.reject(new Error(err.message ?? "client request failed"));
|
|
1743
|
+
else pending.resolve(m.result);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Process one inbound message. Returns true if this was a terminal
|
|
1747
|
+
* message (rare; reserved for future use by the server's own
|
|
1748
|
+
* shutdown signal).
|
|
1749
|
+
*/
|
|
1750
|
+
async handleMessage(msg) {
|
|
1751
|
+
if (typeof msg !== "object" || msg === null) return false;
|
|
1752
|
+
const m = msg;
|
|
1753
|
+
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
if (m.id !== void 0 && typeof m.method === "string") {
|
|
1757
|
+
return this.handleRequest(m.id, m.method, m.params);
|
|
1758
|
+
}
|
|
1759
|
+
if (typeof m.method === "string") {
|
|
1760
|
+
return this.handleNotification(m.method, m.params);
|
|
1761
|
+
}
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
/** Abort all active turns and drop session state. */
|
|
1765
|
+
close() {
|
|
1766
|
+
for (const [, session] of this.sessions) {
|
|
1767
|
+
session.abort.abort();
|
|
1768
|
+
}
|
|
1769
|
+
this.sessions.clear();
|
|
1770
|
+
for (const [, p] of this.pendingOut) {
|
|
1771
|
+
clearTimeout(p.timer);
|
|
1772
|
+
p.reject(new Error("protocol handler closed"));
|
|
1773
|
+
}
|
|
1774
|
+
this.pendingOut.clear();
|
|
1775
|
+
}
|
|
1776
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1777
|
+
// Requests
|
|
1778
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1779
|
+
async handleRequest(id, method, params) {
|
|
1780
|
+
if (method !== "initialize" && !this.initialized) {
|
|
1781
|
+
await this.sendError(id, -32e3, "Not initialized");
|
|
1782
|
+
return false;
|
|
1783
|
+
}
|
|
1784
|
+
try {
|
|
1785
|
+
switch (method) {
|
|
1786
|
+
case "initialize":
|
|
1787
|
+
return await this.handleInitialize(id, params);
|
|
1788
|
+
case "authenticate":
|
|
1789
|
+
return await this.handleAuthenticate(id, params);
|
|
1790
|
+
case "logout":
|
|
1791
|
+
return await this.handleLogout(id, params);
|
|
1792
|
+
case "session/new":
|
|
1793
|
+
return await this.handleSessionNew(id, params);
|
|
1794
|
+
case "session/load":
|
|
1795
|
+
return await this.handleSessionLoad(id, params);
|
|
1796
|
+
case "session/resume":
|
|
1797
|
+
return await this.handleSessionResume(id, params);
|
|
1798
|
+
case "session/close":
|
|
1799
|
+
return await this.handleSessionClose(id, params);
|
|
1800
|
+
case "session/delete":
|
|
1801
|
+
return await this.handleSessionDelete(id, params);
|
|
1802
|
+
case "session/prompt":
|
|
1803
|
+
return await this.handleSessionPrompt(id, params);
|
|
1804
|
+
case "session/set_mode":
|
|
1805
|
+
return await this.handleSetMode(id, params);
|
|
1806
|
+
case "session/set_config_option":
|
|
1807
|
+
return await this.handleSetConfigOption(id, params);
|
|
1808
|
+
case "session/list":
|
|
1809
|
+
return await this.handleSessionList(id);
|
|
1810
|
+
case "session/fork":
|
|
1811
|
+
return await this.handleSessionFork(id, params);
|
|
1812
|
+
case "providers/list":
|
|
1813
|
+
return await this.handleProvidersList(id, params);
|
|
1814
|
+
case "providers/set":
|
|
1815
|
+
return await this.handleProvidersSet(id, params);
|
|
1816
|
+
case "providers/disable":
|
|
1817
|
+
return await this.handleProvidersDisable(id, params);
|
|
1818
|
+
case "mcp/message":
|
|
1819
|
+
return await this.handleMcpMessage(id, params);
|
|
1820
|
+
default:
|
|
1821
|
+
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
1822
|
+
return false;
|
|
1823
|
+
}
|
|
1824
|
+
} catch (err) {
|
|
1825
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
1826
|
+
await this.sendError(id, code, message, data);
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
async handleInitialize(id, params) {
|
|
1831
|
+
const p = params ?? {};
|
|
1832
|
+
if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
|
|
1833
|
+
this.clientCapabilities = p.clientCapabilities;
|
|
1834
|
+
}
|
|
1835
|
+
this.initialized = true;
|
|
1836
|
+
await this.transport.send(toWire({
|
|
1837
|
+
jsonrpc: "2.0",
|
|
1838
|
+
id,
|
|
1839
|
+
result: {
|
|
1840
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
1841
|
+
agentCapabilities: {
|
|
1842
|
+
loadSession: true,
|
|
1843
|
+
promptCapabilities: {
|
|
1844
|
+
// We route ACP image blocks into the core agent's multimodal
|
|
1845
|
+
// input (server-agent-turn.promptToAgentInput); whether the
|
|
1846
|
+
// model can see them is the configured provider's concern.
|
|
1847
|
+
image: true,
|
|
1848
|
+
audio: false,
|
|
1849
|
+
embeddedContext: true
|
|
1850
|
+
},
|
|
1851
|
+
mcpCapabilities: {
|
|
1852
|
+
http: false,
|
|
1853
|
+
sse: false
|
|
1854
|
+
},
|
|
1855
|
+
sessionCapabilities: {
|
|
1856
|
+
close: {},
|
|
1857
|
+
list: {},
|
|
1858
|
+
delete: {},
|
|
1859
|
+
resume: {}
|
|
1860
|
+
},
|
|
1861
|
+
auth: {
|
|
1862
|
+
logout: {}
|
|
1863
|
+
}
|
|
1864
|
+
},
|
|
1865
|
+
agentInfo: {
|
|
1866
|
+
name: this.agentName,
|
|
1867
|
+
title: "WrongStack",
|
|
1868
|
+
version: WRONGSTACK_VERSION
|
|
1869
|
+
},
|
|
1870
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
1871
|
+
modes: this.modes,
|
|
1872
|
+
configOptions: this.configOptions
|
|
1873
|
+
}
|
|
1874
|
+
}));
|
|
1875
|
+
return false;
|
|
1876
|
+
}
|
|
1877
|
+
async handleAuthenticate(id, _params) {
|
|
1878
|
+
await this.transport.send(toWire({
|
|
1879
|
+
jsonrpc: "2.0",
|
|
1880
|
+
id,
|
|
1881
|
+
result: { outcome: "unauthenticated" }
|
|
1882
|
+
}));
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
async handleLogout(id, _params) {
|
|
1886
|
+
await this.transport.send(toWire({
|
|
1887
|
+
jsonrpc: "2.0",
|
|
1888
|
+
id,
|
|
1889
|
+
result: {}
|
|
1890
|
+
}));
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
async handleSessionNew(id, params) {
|
|
1894
|
+
const p = params ?? {};
|
|
1895
|
+
const cwd = typeof p.cwd === "string" ? p.cwd : this.defaultCwd;
|
|
1896
|
+
const sessionId = `sess_${this.allocId()}`;
|
|
1897
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1898
|
+
const state = {
|
|
1899
|
+
id: sessionId,
|
|
1900
|
+
cwd,
|
|
1901
|
+
abort: new AbortController(),
|
|
1902
|
+
modeId: DEFAULT_MODE_ID,
|
|
1903
|
+
createdAt: now,
|
|
1904
|
+
updatedAt: now
|
|
1905
|
+
};
|
|
1906
|
+
this.sessions.set(sessionId, state);
|
|
1907
|
+
this.onSessionNew(state);
|
|
1908
|
+
await this.persist(state);
|
|
1909
|
+
await this.sendNotification({
|
|
1910
|
+
sessionId,
|
|
1911
|
+
update: {
|
|
1912
|
+
sessionUpdate: "current_mode_update",
|
|
1913
|
+
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
if (this.configOptions.length > 0) {
|
|
1917
|
+
await this.sendNotification({
|
|
1918
|
+
sessionId,
|
|
1919
|
+
update: {
|
|
1920
|
+
sessionUpdate: "config_option_update",
|
|
1921
|
+
configOptions: [...this.configOptions]
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
await this.transport.send(toWire({
|
|
1926
|
+
jsonrpc: "2.0",
|
|
1927
|
+
id,
|
|
1928
|
+
result: {
|
|
1929
|
+
sessionId,
|
|
1930
|
+
modes: this.modes,
|
|
1931
|
+
configOptions: this.configOptions
|
|
1932
|
+
}
|
|
1933
|
+
}));
|
|
1934
|
+
return false;
|
|
1935
|
+
}
|
|
1936
|
+
async handleSessionLoad(id, params) {
|
|
1937
|
+
const p = params ?? {};
|
|
1938
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
1939
|
+
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
1940
|
+
let existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
1941
|
+
if (!existing && sessionId && this.store) {
|
|
1942
|
+
const persisted = await this.store.load(sessionId);
|
|
1943
|
+
if (persisted) {
|
|
1944
|
+
const restored = {
|
|
1945
|
+
id: sessionId,
|
|
1946
|
+
cwd: persisted.cwd ?? loadCwd ?? this.defaultCwd,
|
|
1947
|
+
abort: new AbortController(),
|
|
1948
|
+
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
1949
|
+
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1950
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1951
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
1952
|
+
};
|
|
1953
|
+
this.sessions.set(sessionId, restored);
|
|
1954
|
+
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
1955
|
+
for (const update of persisted.history ?? []) {
|
|
1956
|
+
await this.sendNotification({ sessionId, update });
|
|
1957
|
+
}
|
|
1958
|
+
await this.sendNotification({
|
|
1959
|
+
sessionId,
|
|
1960
|
+
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
1961
|
+
});
|
|
1962
|
+
await this.transport.send(toWire({
|
|
1963
|
+
jsonrpc: "2.0",
|
|
1964
|
+
id,
|
|
1965
|
+
result: {
|
|
1966
|
+
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
1967
|
+
}
|
|
1968
|
+
}));
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (existing) {
|
|
1973
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1974
|
+
const replay = sessionId ? this.replayFor?.(sessionId) : void 0;
|
|
1975
|
+
if (replay) {
|
|
1976
|
+
for (const update of replay) {
|
|
1977
|
+
await this.sendNotification({ sessionId, update });
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
await this.sendNotification({
|
|
1981
|
+
sessionId,
|
|
1982
|
+
update: {
|
|
1983
|
+
sessionUpdate: "session_info_update",
|
|
1984
|
+
updatedAt: existing.updatedAt
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
await this.sendNotification({
|
|
1988
|
+
sessionId,
|
|
1989
|
+
update: {
|
|
1990
|
+
sessionUpdate: "current_mode_update",
|
|
1991
|
+
modeId: existing.modeId
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
await this.transport.send(toWire({
|
|
1995
|
+
jsonrpc: "2.0",
|
|
1996
|
+
id,
|
|
1997
|
+
result: {
|
|
1998
|
+
initialMode: {
|
|
1999
|
+
currentModeId: existing.modeId,
|
|
2000
|
+
availableModes: this.modes
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}));
|
|
2004
|
+
return false;
|
|
2005
|
+
}
|
|
2006
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
2007
|
+
return false;
|
|
2008
|
+
}
|
|
2009
|
+
async handleSessionResume(id, params) {
|
|
2010
|
+
const p = params ?? {};
|
|
2011
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2012
|
+
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2013
|
+
if (existing) {
|
|
2014
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2015
|
+
await this.transport.send(toWire({
|
|
2016
|
+
jsonrpc: "2.0",
|
|
2017
|
+
id,
|
|
2018
|
+
result: {
|
|
2019
|
+
initialMode: {
|
|
2020
|
+
currentModeId: existing.modeId,
|
|
2021
|
+
availableModes: this.modes
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}));
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
2028
|
+
return false;
|
|
2029
|
+
}
|
|
2030
|
+
async handleSessionClose(id, params) {
|
|
2031
|
+
const p = params ?? {};
|
|
2032
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2033
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2034
|
+
if (!session) {
|
|
2035
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
2036
|
+
return false;
|
|
2037
|
+
}
|
|
2038
|
+
session.abort.abort();
|
|
2039
|
+
if (sessionId) this.sessions.delete(sessionId);
|
|
2040
|
+
await this.transport.send(toWire({
|
|
2041
|
+
jsonrpc: "2.0",
|
|
2042
|
+
id,
|
|
2043
|
+
result: {}
|
|
2044
|
+
}));
|
|
2045
|
+
return false;
|
|
2046
|
+
}
|
|
2047
|
+
async handleSessionDelete(id, params) {
|
|
2048
|
+
const p = params ?? {};
|
|
2049
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2050
|
+
if (!sessionId) {
|
|
2051
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
if (!this.sessions.has(sessionId)) {
|
|
2055
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
2056
|
+
return false;
|
|
2057
|
+
}
|
|
2058
|
+
const session = this.sessions.get(sessionId);
|
|
2059
|
+
session.abort.abort();
|
|
2060
|
+
this.sessions.delete(sessionId);
|
|
2061
|
+
await this.transport.send(toWire({
|
|
2062
|
+
jsonrpc: "2.0",
|
|
2063
|
+
id,
|
|
2064
|
+
result: {}
|
|
2065
|
+
}));
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
async handleSessionFork(id, params) {
|
|
2069
|
+
const p = params ?? {};
|
|
2070
|
+
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2071
|
+
if (!sourceId || !this.sessions.has(sourceId)) {
|
|
2072
|
+
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
const forkParams = params;
|
|
2076
|
+
return this.handleSessionNew(id, { ...forkParams, cwd: p.cwd ?? this.defaultCwd });
|
|
2077
|
+
}
|
|
2078
|
+
async handleProvidersList(id, _params) {
|
|
2079
|
+
await this.transport.send(toWire({
|
|
2080
|
+
jsonrpc: "2.0",
|
|
2081
|
+
id,
|
|
2082
|
+
result: {
|
|
2083
|
+
providers: [],
|
|
2084
|
+
currentProviderId: null
|
|
2085
|
+
}
|
|
2086
|
+
}));
|
|
2087
|
+
return false;
|
|
2088
|
+
}
|
|
2089
|
+
async handleProvidersSet(id, _params) {
|
|
2090
|
+
await this.sendError(id, -32e3, "provider configuration not available through ACP; use wstack auth");
|
|
2091
|
+
return false;
|
|
2092
|
+
}
|
|
2093
|
+
async handleProvidersDisable(id, _params) {
|
|
2094
|
+
await this.transport.send(toWire({
|
|
2095
|
+
jsonrpc: "2.0",
|
|
2096
|
+
id,
|
|
2097
|
+
result: {}
|
|
2098
|
+
}));
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
async handleMcpMessage(id, _params) {
|
|
2102
|
+
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
2103
|
+
return false;
|
|
2104
|
+
}
|
|
2105
|
+
async handleSessionPrompt(id, params) {
|
|
2106
|
+
const p = params ?? {};
|
|
2107
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2108
|
+
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
2109
|
+
await this.sendError(id, -32e3, "unknown or missing sessionId");
|
|
2110
|
+
return false;
|
|
2111
|
+
}
|
|
2112
|
+
if (!Array.isArray(p.prompt)) {
|
|
2113
|
+
await this.sendError(id, -32602, "prompt must be an array of content blocks");
|
|
2114
|
+
return false;
|
|
2115
|
+
}
|
|
2116
|
+
const session = this.sessions.get(sessionId);
|
|
2117
|
+
if (session.abort.signal.aborted) {
|
|
2118
|
+
session.abort = new AbortController();
|
|
2119
|
+
}
|
|
2120
|
+
const turnSignal = new AbortController();
|
|
2121
|
+
const onCancel = () => turnSignal.abort();
|
|
2122
|
+
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
2123
|
+
const api = {
|
|
2124
|
+
clientCapabilities: this.clientCapabilities,
|
|
2125
|
+
requestPermission: async (req) => {
|
|
2126
|
+
const res = await this.request("session/request_permission", {
|
|
2127
|
+
sessionId,
|
|
2128
|
+
toolCall: req.toolCall,
|
|
2129
|
+
options: req.options
|
|
2130
|
+
});
|
|
2131
|
+
const outcome = res?.outcome;
|
|
2132
|
+
return outcome ?? { outcome: "cancelled" };
|
|
2133
|
+
},
|
|
2134
|
+
readTextFile: async (params2) => {
|
|
2135
|
+
const res = await this.request("fs/read_text_file", { sessionId, ...params2 });
|
|
2136
|
+
return String(res?.content ?? "");
|
|
2137
|
+
},
|
|
2138
|
+
writeTextFile: async (params2) => {
|
|
2139
|
+
await this.request("fs/write_text_file", { sessionId, ...params2 });
|
|
2140
|
+
},
|
|
2141
|
+
runTerminal: async ({ command, args, cwd }) => {
|
|
2142
|
+
const created = await this.request("terminal/create", {
|
|
2143
|
+
sessionId,
|
|
2144
|
+
command,
|
|
2145
|
+
...args ? { args } : {},
|
|
2146
|
+
...cwd ? { cwd } : {}
|
|
2147
|
+
});
|
|
2148
|
+
const terminalId = created?.terminalId;
|
|
2149
|
+
if (!terminalId) return { output: "", exitCode: null };
|
|
2150
|
+
try {
|
|
2151
|
+
const exit = await this.request("terminal/wait_for_exit", { sessionId, terminalId });
|
|
2152
|
+
const out = await this.request("terminal/output", { sessionId, terminalId });
|
|
2153
|
+
return {
|
|
2154
|
+
output: String(out?.output ?? ""),
|
|
2155
|
+
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
|
|
2156
|
+
};
|
|
2157
|
+
} finally {
|
|
2158
|
+
try {
|
|
2159
|
+
await this.request("terminal/release", { sessionId, terminalId });
|
|
2160
|
+
} catch {
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
let result;
|
|
2166
|
+
try {
|
|
2167
|
+
result = await this.runTurn(
|
|
2168
|
+
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
2169
|
+
(update) => this.sendNotification({ sessionId, update }),
|
|
2170
|
+
api
|
|
2171
|
+
);
|
|
2172
|
+
} catch (err) {
|
|
2173
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
2174
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
2175
|
+
await this.sendError(id, code, message, data);
|
|
2176
|
+
return false;
|
|
2177
|
+
}
|
|
2178
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
2179
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2180
|
+
await this.persist(session);
|
|
2181
|
+
await this.transport.send(toWire({
|
|
2182
|
+
jsonrpc: "2.0",
|
|
2183
|
+
id,
|
|
2184
|
+
result: { stopReason: result.stopReason }
|
|
2185
|
+
}));
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
async handleSetMode(id, params) {
|
|
2189
|
+
const p = params ?? {};
|
|
2190
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2191
|
+
const modeId = typeof p.modeId === "string" ? p.modeId : null;
|
|
2192
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2193
|
+
if (!session || !modeId || !this.modes.some((m) => m.id === modeId)) {
|
|
2194
|
+
await this.sendError(id, -32602, "invalid sessionId or modeId");
|
|
2195
|
+
return false;
|
|
2196
|
+
}
|
|
2197
|
+
session.modeId = modeId;
|
|
2198
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2199
|
+
await this.sendNotification({
|
|
2200
|
+
sessionId,
|
|
2201
|
+
update: { sessionUpdate: "current_mode_update", modeId }
|
|
2202
|
+
});
|
|
2203
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
|
|
2204
|
+
return false;
|
|
2205
|
+
}
|
|
2206
|
+
async handleSetConfigOption(id, params) {
|
|
2207
|
+
const p = params ?? {};
|
|
2208
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2209
|
+
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
2210
|
+
const value = typeof p.value === "string" ? p.value : null;
|
|
2211
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2212
|
+
const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
|
|
2213
|
+
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
2214
|
+
await this.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
option.currentValue = value;
|
|
2218
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2219
|
+
await this.sendNotification({
|
|
2220
|
+
sessionId,
|
|
2221
|
+
update: {
|
|
2222
|
+
sessionUpdate: "config_option_update",
|
|
2223
|
+
configOptions: [...this.configOptions]
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
2227
|
+
return false;
|
|
2228
|
+
}
|
|
2229
|
+
async handleSessionList(id) {
|
|
2230
|
+
const sessions = Array.from(this.sessions.values()).map((s) => {
|
|
2231
|
+
const out = {
|
|
2232
|
+
sessionId: s.id,
|
|
2233
|
+
cwd: s.cwd,
|
|
2234
|
+
updatedAt: s.updatedAt
|
|
2235
|
+
};
|
|
2236
|
+
if (s.title !== void 0) out.title = s.title;
|
|
2237
|
+
return out;
|
|
2238
|
+
});
|
|
2239
|
+
await this.transport.send(toWire({
|
|
2240
|
+
jsonrpc: "2.0",
|
|
2241
|
+
id,
|
|
2242
|
+
result: { sessions }
|
|
2243
|
+
}));
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
// ────────────────────────────────────────────────────────────────────
|
|
2247
|
+
// Notifications
|
|
2248
|
+
// ────────────────────────────────────────────────────────────────────
|
|
2249
|
+
async handleNotification(method, params) {
|
|
2250
|
+
switch (method) {
|
|
2251
|
+
case "session/cancel": {
|
|
2252
|
+
const p = params ?? {};
|
|
2253
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2254
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2255
|
+
if (session) {
|
|
2256
|
+
session.abort.abort();
|
|
2257
|
+
}
|
|
2258
|
+
return false;
|
|
2259
|
+
}
|
|
2260
|
+
case "$/cancel_request": {
|
|
2261
|
+
return false;
|
|
2262
|
+
}
|
|
2263
|
+
case "exit":
|
|
2264
|
+
this.close();
|
|
2265
|
+
return true;
|
|
2266
|
+
default:
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
// ────────────────────────────────────────────────────────────────────
|
|
2271
|
+
// Wire helpers
|
|
2272
|
+
// ────────────────────────────────────────────────────────────────────
|
|
2273
|
+
async sendNotification(params) {
|
|
2274
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
|
|
2275
|
+
}
|
|
2276
|
+
/** Best-effort durable persistence of a session + its recorded history. */
|
|
2277
|
+
async persist(state) {
|
|
2278
|
+
if (!this.store) return;
|
|
2279
|
+
try {
|
|
2280
|
+
await this.store.save(state, this.replayFor?.(state.id));
|
|
2281
|
+
} catch {
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
async sendError(id, code, message, data) {
|
|
2285
|
+
const error = { code, message };
|
|
2286
|
+
if (data !== void 0) error.data = data;
|
|
2287
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
2288
|
+
}
|
|
2289
|
+
allocId() {
|
|
2290
|
+
return this.nextId++;
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
function errorToJsonRpc(err) {
|
|
2294
|
+
if (err && typeof err === "object") {
|
|
2295
|
+
const e = err;
|
|
2296
|
+
if (typeof e.code === "number" && typeof e.message === "string") {
|
|
2297
|
+
const result = {
|
|
2298
|
+
code: e.code,
|
|
2299
|
+
message: e.message
|
|
2300
|
+
};
|
|
2301
|
+
if (e.data !== void 0) result.data = e.data;
|
|
2302
|
+
return result;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2306
|
+
return { code: -32603, message };
|
|
2307
|
+
}
|
|
2308
|
+
var WrongStackACPServer = class {
|
|
2309
|
+
transport;
|
|
2310
|
+
handler;
|
|
2311
|
+
options;
|
|
2312
|
+
/** HTTP server when transport mode is HTTP. */
|
|
2313
|
+
httpServer = null;
|
|
2314
|
+
running = false;
|
|
2315
|
+
constructor(opts = {}) {
|
|
2316
|
+
this.options = opts;
|
|
2317
|
+
this.transport = new StdioTransport();
|
|
2318
|
+
const runTurn = opts.runTurn ?? defaultEchoRunTurn;
|
|
2319
|
+
this.handler = new ACPProtocolHandler({
|
|
2320
|
+
transport: this.transport,
|
|
2321
|
+
defaultCwd: opts.defaultCwd ?? process.cwd(),
|
|
2322
|
+
runTurn,
|
|
2323
|
+
agentName: opts.agentName,
|
|
2324
|
+
...opts.replayFor ? { replayFor: opts.replayFor } : {},
|
|
2325
|
+
...opts.seedFor ? { seedFor: opts.seedFor } : {},
|
|
2326
|
+
...opts.store ? { store: opts.store } : {}
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Start the server. Mode depends on `options.transport`:
|
|
2331
|
+
* - 'stdio' (default): reads JSON-RPC from stdin, writes to stdout.
|
|
2332
|
+
* - number: listens as HTTP on the given port.
|
|
2333
|
+
*/
|
|
2334
|
+
async start() {
|
|
2335
|
+
const transportMode = this.options.transport;
|
|
2336
|
+
if (typeof transportMode === "number") {
|
|
2337
|
+
await this.startHttp(transportMode);
|
|
2338
|
+
} else {
|
|
2339
|
+
await this.startStdio();
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
async startStdio() {
|
|
2343
|
+
if (this.options.legacyStartupMarker) {
|
|
2344
|
+
this.transport.sendStartupMarker();
|
|
2345
|
+
}
|
|
2346
|
+
this.running = true;
|
|
2347
|
+
while (this.running) {
|
|
2348
|
+
const msg = await this.transport.read();
|
|
2349
|
+
if (!msg) break;
|
|
2350
|
+
const terminal = await this.handler.handleMessage(msg);
|
|
2351
|
+
if (terminal) break;
|
|
2352
|
+
}
|
|
2353
|
+
this.transport.close();
|
|
2354
|
+
}
|
|
2355
|
+
async startHttp(port) {
|
|
2356
|
+
const host = this.options.host ?? "127.0.0.1";
|
|
2357
|
+
const handler = this.handler;
|
|
2358
|
+
this.httpServer = createServer(async (req, res) => {
|
|
2359
|
+
const selfOrigin = `http://${host}:${port}`;
|
|
2360
|
+
const reqOrigin = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin;
|
|
2361
|
+
if (reqOrigin && reqOrigin !== selfOrigin) {
|
|
2362
|
+
res.writeHead(403);
|
|
2363
|
+
res.end(JSON.stringify({ error: "cross-origin request forbidden" }));
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
if (reqOrigin) res.setHeader("Access-Control-Allow-Origin", reqOrigin);
|
|
2367
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
2368
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
|
2369
|
+
if (req.method === "OPTIONS") {
|
|
2370
|
+
res.writeHead(204);
|
|
2371
|
+
res.end();
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
if (req.method !== "POST") {
|
|
2375
|
+
res.writeHead(405);
|
|
2376
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
let body = "";
|
|
2380
|
+
for await (const chunk of req) {
|
|
2381
|
+
body += chunk;
|
|
2382
|
+
}
|
|
2383
|
+
let msg;
|
|
2384
|
+
try {
|
|
2385
|
+
msg = JSON.parse(body);
|
|
2386
|
+
} catch {
|
|
2387
|
+
res.writeHead(400);
|
|
2388
|
+
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
const notifications = [];
|
|
2392
|
+
let response = null;
|
|
2393
|
+
const originalSend = this.transport.send.bind(this.transport);
|
|
2394
|
+
this.transport.send = async (m) => {
|
|
2395
|
+
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
2396
|
+
response = m;
|
|
2397
|
+
} else if (m.method === "session/update") {
|
|
2398
|
+
notifications.push(m.params);
|
|
2399
|
+
} else {
|
|
2400
|
+
notifications.push(m);
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
try {
|
|
2404
|
+
await handler.handleMessage(msg);
|
|
2405
|
+
} finally {
|
|
2406
|
+
this.transport.send = originalSend;
|
|
2407
|
+
}
|
|
2408
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2409
|
+
const responseBody = response !== null ? { ...response, notifications } : { notifications };
|
|
2410
|
+
res.end(JSON.stringify(responseBody));
|
|
2411
|
+
});
|
|
2412
|
+
return new Promise((resolve3) => {
|
|
2413
|
+
this.httpServer.listen(port, host, () => {
|
|
2414
|
+
writeErr(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
2415
|
+
`);
|
|
2416
|
+
this.running = true;
|
|
2417
|
+
resolve3();
|
|
2418
|
+
});
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
/** Stop the server. */
|
|
2422
|
+
stop() {
|
|
2423
|
+
this.running = false;
|
|
2424
|
+
this.transport.close();
|
|
2425
|
+
if (this.httpServer) {
|
|
2426
|
+
this.httpServer.close();
|
|
2427
|
+
this.httpServer = null;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
2432
|
+
return { stopReason: "end_turn" };
|
|
2433
|
+
};
|
|
2434
|
+
async function main() {
|
|
2435
|
+
const server = new WrongStackACPServer();
|
|
2436
|
+
await server.start();
|
|
2437
|
+
}
|
|
2438
|
+
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
2439
|
+
if (isEntrypoint) {
|
|
2440
|
+
main().catch((err) => {
|
|
2441
|
+
writeErr(`[wstack-acp fatal] ${err}
|
|
2442
|
+
`);
|
|
2443
|
+
process.exit(1);
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// src/agent/server-agent-turn.ts
|
|
2448
|
+
function makeACPServerAgentTurn(opts) {
|
|
2449
|
+
const agents = /* @__PURE__ */ new Map();
|
|
2450
|
+
const timeouts = /* @__PURE__ */ new Map();
|
|
2451
|
+
const history = /* @__PURE__ */ new Map();
|
|
2452
|
+
const pendingSeed = /* @__PURE__ */ new Set();
|
|
2453
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
2454
|
+
const turn = async (input, emit, api) => {
|
|
2455
|
+
let agent = agents.get(input.sessionId);
|
|
2456
|
+
if (!agent) {
|
|
2457
|
+
agent = await opts.agentFor(input.sessionId, process.cwd(), api);
|
|
2458
|
+
agents.set(input.sessionId, agent);
|
|
2459
|
+
if (pendingSeed.has(input.sessionId)) {
|
|
2460
|
+
pendingSeed.delete(input.sessionId);
|
|
2461
|
+
seedAgentContext(agent, history.get(input.sessionId) ?? []);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
const turnAbort = new AbortController();
|
|
2465
|
+
const abortForTimeout = () => turnAbort.abort();
|
|
2466
|
+
const onParentAbort = () => turnAbort.abort();
|
|
2467
|
+
if (input.signal.aborted) {
|
|
2468
|
+
turnAbort.abort();
|
|
2469
|
+
} else {
|
|
2470
|
+
input.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
2471
|
+
}
|
|
2472
|
+
const timer = setTimeout(() => {
|
|
2473
|
+
timeouts.delete(input.sessionId);
|
|
2474
|
+
abortForTimeout();
|
|
2475
|
+
}, timeoutMs);
|
|
2476
|
+
timeouts.set(input.sessionId, timer);
|
|
2477
|
+
const unsub = [];
|
|
2478
|
+
const bus = agent.events;
|
|
2479
|
+
if (bus?.on) {
|
|
2480
|
+
unsub.push(
|
|
2481
|
+
bus.on("tool.started", (e) => {
|
|
2482
|
+
emit({
|
|
2483
|
+
sessionUpdate: "tool_call",
|
|
2484
|
+
toolCallId: e.id,
|
|
2485
|
+
title: toolTitle(e.name, e.input),
|
|
2486
|
+
kind: toolNameToKind(e.name),
|
|
2487
|
+
status: "in_progress",
|
|
2488
|
+
...isRecord2(e.input) ? { rawInput: e.input } : {}
|
|
2489
|
+
});
|
|
2490
|
+
}),
|
|
2491
|
+
bus.on("tool.executed", (e) => {
|
|
2492
|
+
emit({
|
|
2493
|
+
sessionUpdate: "tool_call_update",
|
|
2494
|
+
toolCallId: e.id ?? e.name,
|
|
2495
|
+
status: e.ok ? "completed" : "failed",
|
|
2496
|
+
...e.output !== void 0 ? {
|
|
2497
|
+
content: [
|
|
2498
|
+
{ type: "content", content: { type: "text", text: e.output } }
|
|
2499
|
+
]
|
|
2500
|
+
} : {}
|
|
2501
|
+
});
|
|
2502
|
+
})
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
try {
|
|
2506
|
+
const userInput = promptToAgentInput(input.prompt);
|
|
2507
|
+
const result = await agent.run(userInput, { signal: turnAbort.signal });
|
|
2508
|
+
const text = extractText2(result);
|
|
2509
|
+
if (text) {
|
|
2510
|
+
emit({
|
|
2511
|
+
sessionUpdate: "agent_message_chunk",
|
|
2512
|
+
content: { type: "text", text }
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
const userText = promptToText(input.prompt);
|
|
2516
|
+
const hist = history.get(input.sessionId) ?? [];
|
|
2517
|
+
if (userText) {
|
|
2518
|
+
hist.push({ sessionUpdate: "user_message_chunk", content: { type: "text", text: userText } });
|
|
2519
|
+
}
|
|
2520
|
+
if (text) {
|
|
2521
|
+
hist.push({ sessionUpdate: "agent_message_chunk", content: { type: "text", text } });
|
|
2522
|
+
}
|
|
2523
|
+
if (hist.length > 0) history.set(input.sessionId, hist);
|
|
2524
|
+
const plan = extractPlan(result);
|
|
2525
|
+
if (plan.length > 0) {
|
|
2526
|
+
emit({
|
|
2527
|
+
sessionUpdate: "plan",
|
|
2528
|
+
entries: plan
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
const usage = extractUsage(result);
|
|
2532
|
+
if (usage) {
|
|
2533
|
+
emit({
|
|
2534
|
+
sessionUpdate: "usage_update",
|
|
2535
|
+
used: usage.used,
|
|
2536
|
+
size: usage.size,
|
|
2537
|
+
...usage.cost ? { cost: usage.cost } : {}
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
const result_out = {
|
|
2541
|
+
// `turnAbort.signal` covers both client cancellation and the
|
|
2542
|
+
// wall-clock timeout, so either maps to stopReason 'cancelled'.
|
|
2543
|
+
stopReason: pickStopReason(result, turnAbort.signal)
|
|
2544
|
+
};
|
|
2545
|
+
if (text) result_out.text = text;
|
|
2546
|
+
const runTurnPlan = extractPlan(result);
|
|
2547
|
+
if (runTurnPlan.length > 0) result_out.plan = runTurnPlan;
|
|
2548
|
+
if (usage) result_out.usage = usage;
|
|
2549
|
+
return result_out;
|
|
2550
|
+
} finally {
|
|
2551
|
+
clearTimeout(timer);
|
|
2552
|
+
timeouts.delete(input.sessionId);
|
|
2553
|
+
input.signal.removeEventListener("abort", onParentAbort);
|
|
2554
|
+
for (const u of unsub) u();
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2557
|
+
const replay = (sessionId) => history.get(sessionId) ?? [];
|
|
2558
|
+
const seed = (sessionId, incoming) => {
|
|
2559
|
+
if (incoming.length === 0) return;
|
|
2560
|
+
history.set(sessionId, [...incoming]);
|
|
2561
|
+
pendingSeed.add(sessionId);
|
|
2562
|
+
};
|
|
2563
|
+
return Object.assign(turn, { replay, seed });
|
|
2564
|
+
}
|
|
2565
|
+
function seedAgentContext(agent, history) {
|
|
2566
|
+
const state = agent.ctx?.state;
|
|
2567
|
+
if (!state?.appendMessage) return;
|
|
2568
|
+
for (const u of history) {
|
|
2569
|
+
const text = u.content?.text;
|
|
2570
|
+
if (typeof text !== "string" || text.length === 0) continue;
|
|
2571
|
+
const role = u.sessionUpdate === "user_message_chunk" ? "user" : "assistant";
|
|
2572
|
+
state.appendMessage({ role, content: text });
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
function toolNameToKind(name) {
|
|
2576
|
+
const n = name.toLowerCase();
|
|
2577
|
+
if (n.includes("read") || n.includes("cat")) return "read";
|
|
2578
|
+
if (n.includes("write") || n.includes("edit") || n.includes("apply") || n.includes("patch")) return "edit";
|
|
2579
|
+
if (n.includes("delete") || n.includes("rm")) return "delete";
|
|
2580
|
+
if (n.includes("move") || n.includes("rename") || n.includes("mv")) return "move";
|
|
2581
|
+
if (n.includes("grep") || n.includes("glob") || n.includes("search") || n.includes("find")) return "search";
|
|
2582
|
+
if (n.includes("bash") || n.includes("shell") || n.includes("exec") || n.includes("run") || n.includes("terminal")) return "execute";
|
|
2583
|
+
if (n.includes("fetch") || n.includes("http") || n.includes("web") || n.includes("url")) return "fetch";
|
|
2584
|
+
if (n.includes("think") || n.includes("plan")) return "think";
|
|
2585
|
+
return "other";
|
|
2586
|
+
}
|
|
2587
|
+
function toolTitle(name, input) {
|
|
2588
|
+
if (isRecord2(input)) {
|
|
2589
|
+
const path4 = input.path ?? input.file ?? input.filePath ?? input.pattern ?? input.command;
|
|
2590
|
+
if (typeof path4 === "string" && path4.length > 0) {
|
|
2591
|
+
return `${name}: ${path4.length > 80 ? `${path4.slice(0, 77)}\u2026` : path4}`;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
return name;
|
|
2595
|
+
}
|
|
2596
|
+
function isRecord2(v) {
|
|
2597
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2598
|
+
}
|
|
2599
|
+
function promptToAgentInput(blocks) {
|
|
2600
|
+
const hasImage = blocks.some((b) => b.type === "image");
|
|
2601
|
+
if (!hasImage) {
|
|
2602
|
+
return promptToText(blocks);
|
|
2603
|
+
}
|
|
2604
|
+
const out = [];
|
|
2605
|
+
for (const b of blocks) {
|
|
2606
|
+
if (b.type === "text") {
|
|
2607
|
+
out.push({ type: "text", text: b.text });
|
|
2608
|
+
} else if (b.type === "image") {
|
|
2609
|
+
out.push({
|
|
2610
|
+
type: "image",
|
|
2611
|
+
source: { type: "base64", media_type: b.mimeType, data: b.data }
|
|
2612
|
+
});
|
|
2613
|
+
} else if (b.type === "audio") {
|
|
2614
|
+
out.push({ type: "text", text: `[audio: ${b.mimeType}]` });
|
|
2615
|
+
} else if (b.type === "resource") {
|
|
2616
|
+
const text = "text" in b.resource && typeof b.resource.text === "string" ? b.resource.text : `[embedded resource: ${b.resource.uri}]`;
|
|
2617
|
+
out.push({ type: "text", text });
|
|
2618
|
+
} else if (b.type === "resource_link") {
|
|
2619
|
+
out.push({ type: "text", text: `[resource link: ${b.uri}]` });
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return out;
|
|
2623
|
+
}
|
|
2624
|
+
function disposeACPServerAgentTurn(opts) {
|
|
2625
|
+
return Promise.allSettled(
|
|
2626
|
+
Array.from(opts.agents.values()).map((agent) => agent.teardown())
|
|
2627
|
+
).then(() => void 0);
|
|
2628
|
+
}
|
|
2629
|
+
function promptToText(blocks) {
|
|
2630
|
+
const parts = [];
|
|
2631
|
+
for (const b of blocks) {
|
|
2632
|
+
if (b.type === "text") {
|
|
2633
|
+
parts.push(b.text);
|
|
2634
|
+
} else if (b.type === "image") {
|
|
2635
|
+
parts.push(`[image: ${b.mimeType}]`);
|
|
2636
|
+
} else if (b.type === "audio") {
|
|
2637
|
+
parts.push(`[audio: ${b.mimeType}]`);
|
|
2638
|
+
} else if (b.type === "resource") {
|
|
2639
|
+
parts.push(`[embedded resource: ${b.resource.uri}]`);
|
|
2640
|
+
} else if (b.type === "resource_link") {
|
|
2641
|
+
parts.push(`[resource link: ${b.uri}]`);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
return parts.join("\n").trim();
|
|
2645
|
+
}
|
|
2646
|
+
function extractText2(result) {
|
|
2647
|
+
if (typeof result !== "object" || result === null) return "";
|
|
2648
|
+
const r = result;
|
|
2649
|
+
if (typeof r.text === "string") return r.text;
|
|
2650
|
+
if (Array.isArray(r.content)) {
|
|
2651
|
+
const parts = [];
|
|
2652
|
+
for (const c of r.content) {
|
|
2653
|
+
if (typeof c === "object" && c !== null) {
|
|
2654
|
+
const cb = c;
|
|
2655
|
+
if (cb.type === "text" && typeof cb.text === "string") parts.push(cb.text);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
return parts.join("");
|
|
2659
|
+
}
|
|
2660
|
+
return "";
|
|
2661
|
+
}
|
|
2662
|
+
function pickStopReason(result, signal) {
|
|
2663
|
+
if (signal.aborted) return "cancelled";
|
|
2664
|
+
if (typeof result !== "object" || result === null) return "end_turn";
|
|
2665
|
+
const r = result;
|
|
2666
|
+
if (r.error) {
|
|
2667
|
+
return "end_turn";
|
|
2668
|
+
}
|
|
2669
|
+
if (typeof r.stopReason === "string" && r.stopReason) {
|
|
2670
|
+
return r.stopReason;
|
|
2671
|
+
}
|
|
2672
|
+
return "end_turn";
|
|
2673
|
+
}
|
|
2674
|
+
function extractPlan(result) {
|
|
2675
|
+
if (typeof result !== "object" || result === null) return [];
|
|
2676
|
+
const r = result;
|
|
2677
|
+
if (Array.isArray(r.plan)) {
|
|
2678
|
+
return r.plan.filter(
|
|
2679
|
+
(e) => typeof e === "object" && e !== null && typeof e.content === "string"
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
return [];
|
|
2683
|
+
}
|
|
2684
|
+
function extractUsage(result) {
|
|
2685
|
+
if (typeof result !== "object" || result === null) return null;
|
|
2686
|
+
const r = result;
|
|
2687
|
+
if (typeof r.usage === "object" && r.usage !== null) {
|
|
2688
|
+
const u = r.usage;
|
|
2689
|
+
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
2690
|
+
return {
|
|
2691
|
+
used: u.used,
|
|
2692
|
+
size: u.size,
|
|
2693
|
+
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return null;
|
|
2698
|
+
}
|
|
2699
|
+
var ACPSessionStore = class {
|
|
2700
|
+
dir;
|
|
2701
|
+
/**
|
|
2702
|
+
* Memoized result of the first successful `init()`. Saved sessions
|
|
2703
|
+
* are the hot path — calling `mkdir(..., {recursive:true})` on every
|
|
2704
|
+
* turn adds an avoidable syscall to the per-prompt persistence flow.
|
|
2705
|
+
* Cleared automatically if the directory disappears between calls.
|
|
2706
|
+
*/
|
|
2707
|
+
initialized = false;
|
|
2708
|
+
constructor(opts = {}) {
|
|
2709
|
+
this.dir = opts.dir ?? path3.join(process.cwd(), ".acp-sessions");
|
|
2710
|
+
}
|
|
2711
|
+
/** Ensure the store directory exists. Memoized — only mkdirs once. */
|
|
2712
|
+
async init() {
|
|
2713
|
+
if (this.initialized) return;
|
|
2714
|
+
await fsp2.mkdir(this.dir, { recursive: true });
|
|
2715
|
+
this.initialized = true;
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Persist a session state (and optionally its conversation history) to
|
|
2719
|
+
* disk. Returns the session id. `history` enables cross-restart
|
|
2720
|
+
* `session/load` replay.
|
|
2721
|
+
*/
|
|
2722
|
+
async save(state, history) {
|
|
2723
|
+
await this.init();
|
|
2724
|
+
await fsp2.writeFile(
|
|
2725
|
+
path3.join(this.dir, `${state.id}.json`),
|
|
2726
|
+
JSON.stringify({
|
|
2727
|
+
id: state.id,
|
|
2728
|
+
cwd: state.cwd,
|
|
2729
|
+
modeId: state.modeId,
|
|
2730
|
+
createdAt: state.createdAt,
|
|
2731
|
+
updatedAt: state.updatedAt,
|
|
2732
|
+
title: state.title,
|
|
2733
|
+
...history && history.length > 0 ? { history } : {}
|
|
2734
|
+
}),
|
|
2735
|
+
"utf8"
|
|
2736
|
+
);
|
|
2737
|
+
await this.updateIndex(state.id, state.updatedAt);
|
|
2738
|
+
return state.id;
|
|
2739
|
+
}
|
|
2740
|
+
/** Load a persisted session (metadata + history) from disk, or null. */
|
|
2741
|
+
async load(sessionId) {
|
|
2742
|
+
try {
|
|
2743
|
+
const data = await fsp2.readFile(path3.join(this.dir, `${sessionId}.json`), "utf8");
|
|
2744
|
+
return JSON.parse(data);
|
|
2745
|
+
} catch {
|
|
2746
|
+
return null;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
/** List all persisted sessions. */
|
|
2750
|
+
async list() {
|
|
2751
|
+
const indexEntries = await this.readIndex();
|
|
2752
|
+
if (indexEntries !== null) {
|
|
2753
|
+
return indexEntries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2754
|
+
}
|
|
2755
|
+
const files = [];
|
|
2756
|
+
try {
|
|
2757
|
+
const entries = await fsp2.readdir(this.dir);
|
|
2758
|
+
for (const entry of entries) {
|
|
2759
|
+
if (entry.endsWith(".json") && entry !== "index.json") {
|
|
2760
|
+
files.push(entry);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
} catch {
|
|
2764
|
+
return [];
|
|
2765
|
+
}
|
|
2766
|
+
const sessions = [];
|
|
2767
|
+
for (const file of files) {
|
|
2768
|
+
try {
|
|
2769
|
+
const data = await fsp2.readFile(path3.join(this.dir, file), "utf8");
|
|
2770
|
+
const parsed = JSON.parse(data);
|
|
2771
|
+
if (parsed.id) {
|
|
2772
|
+
sessions.push({ id: parsed.id, updatedAt: parsed.updatedAt ?? "" });
|
|
2773
|
+
}
|
|
2774
|
+
} catch {
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2778
|
+
void this.writeIndex(sessions).catch(() => void 0);
|
|
2779
|
+
return sessions;
|
|
2780
|
+
}
|
|
2781
|
+
/** Sidecar path that stores `{id, updatedAt}` for every saved session. */
|
|
2782
|
+
indexPath() {
|
|
2783
|
+
return path3.join(this.dir, "index.json");
|
|
2784
|
+
}
|
|
2785
|
+
/** Read the sidecar index. Returns `null` when missing or unreadable. */
|
|
2786
|
+
async readIndex() {
|
|
2787
|
+
try {
|
|
2788
|
+
const data = await fsp2.readFile(this.indexPath(), "utf8");
|
|
2789
|
+
const parsed = JSON.parse(data);
|
|
2790
|
+
if (!Array.isArray(parsed)) return null;
|
|
2791
|
+
const out = [];
|
|
2792
|
+
for (const e of parsed) {
|
|
2793
|
+
if (e && typeof e.id === "string" && typeof e.updatedAt === "string") {
|
|
2794
|
+
out.push({
|
|
2795
|
+
id: e.id,
|
|
2796
|
+
updatedAt: e.updatedAt
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
return out;
|
|
2801
|
+
} catch {
|
|
2802
|
+
return null;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
/** Atomically replace the sidecar index with the supplied entries. */
|
|
2806
|
+
async writeIndex(entries) {
|
|
2807
|
+
const target = this.indexPath();
|
|
2808
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
2809
|
+
await fsp2.writeFile(tmp, JSON.stringify(entries), "utf8");
|
|
2810
|
+
await fsp2.rename(tmp, target);
|
|
2811
|
+
}
|
|
2812
|
+
/** Update one entry in the index, adding it if missing. Best-effort. */
|
|
2813
|
+
async updateIndex(id, updatedAt) {
|
|
2814
|
+
let entries = await this.readIndex();
|
|
2815
|
+
if (entries === null) {
|
|
2816
|
+
await this.list();
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
const i = entries.findIndex((e) => e.id === id);
|
|
2820
|
+
if (i >= 0) entries[i] = { id, updatedAt };
|
|
2821
|
+
else entries.push({ id, updatedAt });
|
|
2822
|
+
try {
|
|
2823
|
+
await this.writeIndex(entries);
|
|
2824
|
+
} catch {
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
/** Delete a session file. */
|
|
2828
|
+
async delete(sessionId) {
|
|
2829
|
+
try {
|
|
2830
|
+
await fsp2.unlink(path3.join(this.dir, `${sessionId}.json`));
|
|
2831
|
+
} catch {
|
|
2832
|
+
}
|
|
2833
|
+
const entries = await this.readIndex();
|
|
2834
|
+
if (entries === null) return;
|
|
2835
|
+
const next = entries.filter((e) => e.id !== sessionId);
|
|
2836
|
+
if (next.length !== entries.length) {
|
|
2837
|
+
try {
|
|
2838
|
+
await this.writeIndex(next);
|
|
2839
|
+
} catch {
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
/** Get the store directory path. */
|
|
2844
|
+
getDirectory() {
|
|
2845
|
+
return this.dir;
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
|
|
2849
|
+
// src/registry/agents.catalog.ts
|
|
2850
|
+
var AGENTS_CATALOG = [
|
|
2851
|
+
// ── Anthropic ────────────────────────────────────────────────────────
|
|
2852
|
+
{
|
|
2853
|
+
id: "claude-code",
|
|
2854
|
+
displayName: "Claude Code",
|
|
2855
|
+
vendor: "anthropic",
|
|
2856
|
+
probe: { command: "claude", args: ["--version"] },
|
|
2857
|
+
// Claude Code does not speak stdio ACP from the bare `claude` binary —
|
|
2858
|
+
// it drops into its interactive TUI. The official ACP adapter
|
|
2859
|
+
// (`@agentclientprotocol/claude-agent-acp`, registry id `claude-acp`)
|
|
2860
|
+
// wraps the logged-in Claude Code CLI and translates ACP ↔ Claude Code.
|
|
2861
|
+
// Verify with `/acp probe claude-code`; override via `config.acp.agents`.
|
|
2862
|
+
acp: { command: "npx", args: ["-y", "@agentclientprotocol/claude-agent-acp"] },
|
|
2863
|
+
supports: {
|
|
2864
|
+
loadSession: true,
|
|
2865
|
+
promptImages: true,
|
|
2866
|
+
terminal: true,
|
|
2867
|
+
fs: true
|
|
2868
|
+
},
|
|
2869
|
+
integration: "adapter",
|
|
2870
|
+
docs: "https://docs.anthropic.com/en/docs/claude-code"
|
|
2871
|
+
},
|
|
2872
|
+
// ── Google ───────────────────────────────────────────────────────────
|
|
2873
|
+
{
|
|
2874
|
+
id: "gemini-cli",
|
|
2875
|
+
displayName: "Gemini CLI",
|
|
2876
|
+
vendor: "google",
|
|
2877
|
+
probe: { command: "gemini", args: ["--version"] },
|
|
2878
|
+
// Gemini CLI (the @google/gemini-cli package, registry id `gemini`)
|
|
2879
|
+
// speaks ACP behind `--acp`. We invoke the locally-installed binary so it
|
|
2880
|
+
// uses the user's existing login. Confirm with `/acp probe gemini-cli`.
|
|
2881
|
+
acp: { command: "gemini", args: ["--acp"] },
|
|
2882
|
+
supports: {
|
|
2883
|
+
loadSession: true,
|
|
2884
|
+
promptImages: true,
|
|
2885
|
+
terminal: true,
|
|
2886
|
+
fs: true
|
|
2887
|
+
},
|
|
2888
|
+
integration: "native",
|
|
2889
|
+
docs: "https://github.com/google-gemini/gemini-cli"
|
|
2890
|
+
},
|
|
2891
|
+
// ── OpenAI ───────────────────────────────────────────────────────────
|
|
2892
|
+
{
|
|
2893
|
+
id: "codex-cli",
|
|
2894
|
+
displayName: "Codex CLI",
|
|
2895
|
+
vendor: "openai",
|
|
2896
|
+
probe: { command: "codex", args: ["--version"] },
|
|
2897
|
+
// Bare `codex` has no stdio-ACP entry; the official adapter
|
|
2898
|
+
// (`@agentclientprotocol/codex-acp`, registry id `codex-acp`) wraps the
|
|
2899
|
+
// logged-in Codex CLI. Confirm with `/acp probe codex-cli`.
|
|
2900
|
+
acp: { command: "npx", args: ["-y", "@agentclientprotocol/codex-acp"] },
|
|
2901
|
+
supports: {
|
|
2902
|
+
loadSession: false,
|
|
2903
|
+
promptImages: false,
|
|
2904
|
+
terminal: true,
|
|
2905
|
+
fs: true
|
|
2906
|
+
},
|
|
2907
|
+
integration: "adapter",
|
|
2908
|
+
docs: "https://github.com/openai/codex"
|
|
2909
|
+
},
|
|
2910
|
+
// ── GitHub ───────────────────────────────────────────────────────────
|
|
2911
|
+
{
|
|
2912
|
+
id: "copilot",
|
|
2913
|
+
displayName: "GitHub Copilot CLI",
|
|
2914
|
+
vendor: "github",
|
|
2915
|
+
probe: { command: "gh", args: ["copilot", "--help"] },
|
|
2916
|
+
// ACP is in the standalone @github/copilot CLI (registry id
|
|
2917
|
+
// `github-copilot-cli`), not the `gh copilot` extension. Use the package.
|
|
2918
|
+
acp: { command: "npx", args: ["-y", "@github/copilot", "--acp"] },
|
|
2919
|
+
supports: {
|
|
2920
|
+
loadSession: false,
|
|
2921
|
+
promptImages: false,
|
|
2922
|
+
terminal: true,
|
|
2923
|
+
fs: false
|
|
2924
|
+
},
|
|
2925
|
+
integration: "experimental",
|
|
2926
|
+
docs: "https://github.com/features/copilot/cli"
|
|
2927
|
+
},
|
|
2928
|
+
// ── Community / wrappers ─────────────────────────────────────────────
|
|
2929
|
+
{
|
|
2930
|
+
id: "cline",
|
|
2931
|
+
displayName: "Cline",
|
|
2932
|
+
vendor: "community",
|
|
2933
|
+
probe: { command: "npx", args: ["--version"] },
|
|
2934
|
+
// Registry id `cline`: the `cline` npm package speaks ACP behind `--acp`.
|
|
2935
|
+
acp: {
|
|
2936
|
+
command: "npx",
|
|
2937
|
+
args: ["-y", "cline", "--acp"]
|
|
2938
|
+
},
|
|
2939
|
+
supports: {
|
|
2940
|
+
loadSession: true,
|
|
2941
|
+
promptImages: true,
|
|
2942
|
+
terminal: true,
|
|
2943
|
+
fs: true
|
|
2944
|
+
},
|
|
2945
|
+
integration: "community",
|
|
2946
|
+
docs: "https://github.com/cline/cline"
|
|
2947
|
+
},
|
|
2948
|
+
{
|
|
2949
|
+
id: "goose",
|
|
2950
|
+
displayName: "Goose",
|
|
2951
|
+
vendor: "community",
|
|
2952
|
+
probe: { command: "goose", args: ["--version"] },
|
|
2953
|
+
acp: { command: "goose", args: ["acp"] },
|
|
2954
|
+
supports: {
|
|
2955
|
+
loadSession: true,
|
|
2956
|
+
promptImages: true,
|
|
2957
|
+
terminal: true,
|
|
2958
|
+
fs: true
|
|
2959
|
+
},
|
|
2960
|
+
integration: "experimental",
|
|
2961
|
+
docs: "https://github.com/block/goose"
|
|
2962
|
+
},
|
|
2963
|
+
{
|
|
2964
|
+
id: "openhands",
|
|
2965
|
+
displayName: "OpenHands",
|
|
2966
|
+
vendor: "community",
|
|
2967
|
+
probe: { command: "openhands", args: ["--version"] },
|
|
2968
|
+
acp: { command: "openhands", args: [] },
|
|
2969
|
+
supports: {
|
|
2970
|
+
loadSession: false,
|
|
2971
|
+
promptImages: true,
|
|
2972
|
+
terminal: true,
|
|
2973
|
+
fs: true
|
|
2974
|
+
},
|
|
2975
|
+
integration: "experimental",
|
|
2976
|
+
// Canonical repo URL — the org renamed; All-Hands-AI/OpenHands 301-redirects here.
|
|
2977
|
+
docs: "https://github.com/OpenHands/OpenHands"
|
|
2978
|
+
},
|
|
2979
|
+
// ── Vendor CLIs (native binaries) ───────────────────────────────────
|
|
2980
|
+
{
|
|
2981
|
+
id: "qwen-code",
|
|
2982
|
+
displayName: "Qwen Code",
|
|
2983
|
+
vendor: "community",
|
|
2984
|
+
probe: { command: "qwen", args: ["--version"] },
|
|
2985
|
+
// Qwen Code (the @qwen-code/qwen-code package) speaks ACP behind `--acp`.
|
|
2986
|
+
acp: { command: "qwen", args: ["--acp"] },
|
|
2987
|
+
supports: {
|
|
2988
|
+
loadSession: false,
|
|
2989
|
+
promptImages: false,
|
|
2990
|
+
terminal: true,
|
|
2991
|
+
fs: false
|
|
2992
|
+
},
|
|
2993
|
+
integration: "experimental",
|
|
2994
|
+
docs: "https://github.com/QwenLM/Qwen3-Coder"
|
|
2995
|
+
},
|
|
2996
|
+
{
|
|
2997
|
+
id: "kiro-cli",
|
|
2998
|
+
displayName: "Kiro CLI",
|
|
2999
|
+
vendor: "community",
|
|
3000
|
+
probe: { command: "kiro", args: ["--version"] },
|
|
3001
|
+
acp: { command: "kiro", args: [] },
|
|
3002
|
+
supports: {
|
|
3003
|
+
loadSession: false,
|
|
3004
|
+
promptImages: false,
|
|
3005
|
+
terminal: true,
|
|
3006
|
+
fs: true
|
|
3007
|
+
},
|
|
3008
|
+
integration: "experimental",
|
|
3009
|
+
docs: "https://kiro.dev"
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
id: "opencode",
|
|
3013
|
+
displayName: "OpenCode",
|
|
3014
|
+
vendor: "community",
|
|
3015
|
+
probe: { command: "opencode", args: ["--version"] },
|
|
3016
|
+
// OpenCode speaks ACP via its `acp` subcommand (registry id `opencode`).
|
|
3017
|
+
acp: { command: "opencode", args: ["acp"] },
|
|
3018
|
+
supports: {
|
|
3019
|
+
loadSession: true,
|
|
3020
|
+
promptImages: true,
|
|
3021
|
+
terminal: true,
|
|
3022
|
+
fs: true
|
|
3023
|
+
},
|
|
3024
|
+
integration: "native",
|
|
3025
|
+
docs: "https://github.com/sst/opencode"
|
|
3026
|
+
},
|
|
3027
|
+
{
|
|
3028
|
+
id: "mistral-vibe",
|
|
3029
|
+
displayName: "Mistral Vibe",
|
|
3030
|
+
vendor: "community",
|
|
3031
|
+
probe: { command: "vibe", args: ["--version"] },
|
|
3032
|
+
acp: { command: "vibe", args: [] },
|
|
3033
|
+
supports: {
|
|
3034
|
+
loadSession: false,
|
|
3035
|
+
promptImages: false,
|
|
3036
|
+
terminal: true,
|
|
3037
|
+
fs: false
|
|
3038
|
+
},
|
|
3039
|
+
integration: "experimental",
|
|
3040
|
+
docs: "https://github.com/mistralai/mistral-vibe"
|
|
3041
|
+
},
|
|
3042
|
+
{
|
|
3043
|
+
id: "cursor",
|
|
3044
|
+
displayName: "Cursor",
|
|
3045
|
+
vendor: "community",
|
|
3046
|
+
probe: { command: "cursor", args: ["--version"] },
|
|
3047
|
+
// Cursor's ACP entry is the `cursor-agent acp` binary (registry id `cursor`).
|
|
3048
|
+
acp: { command: "cursor-agent", args: ["acp"] },
|
|
3049
|
+
supports: {
|
|
3050
|
+
loadSession: true,
|
|
3051
|
+
promptImages: true,
|
|
3052
|
+
terminal: true,
|
|
3053
|
+
fs: true
|
|
3054
|
+
},
|
|
3055
|
+
integration: "experimental",
|
|
3056
|
+
docs: "https://cursor.com"
|
|
3057
|
+
}
|
|
3058
|
+
];
|
|
3059
|
+
function findAgentDescriptor(id) {
|
|
3060
|
+
return AGENTS_CATALOG.find((a) => a.id === id);
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// src/integration/acp-subagent-runner.ts
|
|
3064
|
+
var ACP_AGENT_COMMANDS = {
|
|
3065
|
+
cline: {
|
|
3066
|
+
command: "npx",
|
|
3067
|
+
args: ["-y", "@agentify/cline"],
|
|
3068
|
+
role: "cline"
|
|
3069
|
+
},
|
|
3070
|
+
"gemini-cli": {
|
|
3071
|
+
command: "gemini",
|
|
3072
|
+
role: "gemini-cli"
|
|
3073
|
+
},
|
|
3074
|
+
copilot: {
|
|
3075
|
+
command: "gh",
|
|
3076
|
+
args: ["copilot", "agent"],
|
|
3077
|
+
role: "copilot"
|
|
3078
|
+
},
|
|
3079
|
+
openhands: {
|
|
3080
|
+
command: "openhands",
|
|
3081
|
+
role: "openhands"
|
|
3082
|
+
},
|
|
3083
|
+
goose: {
|
|
3084
|
+
command: "goose",
|
|
3085
|
+
role: "goose"
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
async function makeACPSubagentRunner(options) {
|
|
3089
|
+
const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
|
|
3090
|
+
const wrappedRunner = async (task, ctx) => {
|
|
3091
|
+
try {
|
|
3092
|
+
return await runner(task, ctx);
|
|
3093
|
+
} finally {
|
|
3094
|
+
stop();
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
return wrappedRunner;
|
|
3098
|
+
}
|
|
3099
|
+
async function makeACPSubagentRunnerWithStop(options) {
|
|
3100
|
+
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
3101
|
+
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
3102
|
+
const persistent = options.persistent === true;
|
|
3103
|
+
let shared = null;
|
|
3104
|
+
const startSession = async () => {
|
|
3105
|
+
return ACPSession.start({
|
|
3106
|
+
command: options.command,
|
|
3107
|
+
...options.args !== void 0 ? { args: options.args } : {},
|
|
3108
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
3109
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
3110
|
+
projectRoot,
|
|
3111
|
+
timeoutMs,
|
|
3112
|
+
role: options.role,
|
|
3113
|
+
...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
|
|
3114
|
+
...options.mcpServers !== void 0 ? { mcpServers: options.mcpServers } : {}
|
|
3115
|
+
});
|
|
3116
|
+
};
|
|
3117
|
+
const runner = async (task, ctx) => {
|
|
3118
|
+
let session;
|
|
3119
|
+
const reuse = persistent && shared !== null;
|
|
3120
|
+
try {
|
|
3121
|
+
session = reuse ? shared : await startSession();
|
|
3122
|
+
if (persistent) shared = session;
|
|
3123
|
+
} catch (err) {
|
|
3124
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
3125
|
+
}
|
|
3126
|
+
const onProgress = (event) => {
|
|
3127
|
+
try {
|
|
3128
|
+
ctx.budget.markActivity();
|
|
3129
|
+
} catch {
|
|
3130
|
+
}
|
|
3131
|
+
options.onProgress?.(event);
|
|
3132
|
+
};
|
|
3133
|
+
try {
|
|
3134
|
+
const result = await session.prompt(
|
|
3135
|
+
[textContent(task.description)],
|
|
3136
|
+
ctx.signal,
|
|
3137
|
+
onProgress
|
|
3138
|
+
);
|
|
3139
|
+
return {
|
|
3140
|
+
result: result.text,
|
|
3141
|
+
iterations: 1,
|
|
3142
|
+
toolCalls: result.toolCalls.length
|
|
3143
|
+
};
|
|
3144
|
+
} catch (err) {
|
|
3145
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
3146
|
+
} finally {
|
|
3147
|
+
if (!persistent) {
|
|
3148
|
+
try {
|
|
3149
|
+
await session.close();
|
|
3150
|
+
} catch {
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
};
|
|
3155
|
+
const stop = async () => {
|
|
3156
|
+
if (shared) {
|
|
3157
|
+
const s = shared;
|
|
3158
|
+
shared = null;
|
|
3159
|
+
try {
|
|
3160
|
+
await s.close();
|
|
3161
|
+
} catch {
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3165
|
+
return { runner, stop };
|
|
3166
|
+
}
|
|
3167
|
+
function acpErrorToSubagentError(err, subagentId) {
|
|
3168
|
+
if (err instanceof ACPSessionError) {
|
|
3169
|
+
const kind = mapACPKind(err.kind);
|
|
3170
|
+
return {
|
|
3171
|
+
kind,
|
|
3172
|
+
message: `${subagentId}: ${err.message}`,
|
|
3173
|
+
retryable: isRetryable(kind),
|
|
3174
|
+
cause: {
|
|
3175
|
+
name: err.name,
|
|
3176
|
+
message: err.message,
|
|
3177
|
+
...err.stack !== void 0 ? { stack: err.stack } : {}
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3182
|
+
return {
|
|
3183
|
+
kind: "bridge_failed",
|
|
3184
|
+
message: `${subagentId}: ${message}`,
|
|
3185
|
+
retryable: false,
|
|
3186
|
+
cause: {
|
|
3187
|
+
name: err instanceof Error ? err.name : "Error",
|
|
3188
|
+
message,
|
|
3189
|
+
...err instanceof Error && err.stack !== void 0 ? { stack: err.stack } : {}
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
function mapACPKind(acpKind) {
|
|
3194
|
+
switch (acpKind) {
|
|
3195
|
+
case "spawn_failed":
|
|
3196
|
+
case "init_failed":
|
|
3197
|
+
case "session_create_failed":
|
|
3198
|
+
case "agent_died":
|
|
3199
|
+
case "protocol_error":
|
|
3200
|
+
return "bridge_failed";
|
|
3201
|
+
case "prompt_failed":
|
|
3202
|
+
return "tool_failed";
|
|
3203
|
+
case "auth_failed":
|
|
3204
|
+
case "logout_failed":
|
|
3205
|
+
return "bridge_failed";
|
|
3206
|
+
case "aborted":
|
|
3207
|
+
return "aborted_by_parent";
|
|
3208
|
+
case "closed":
|
|
3209
|
+
case "unsupported_capability":
|
|
3210
|
+
return "unknown";
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
function isRetryable(kind) {
|
|
3214
|
+
switch (kind) {
|
|
3215
|
+
case "provider_5xx":
|
|
3216
|
+
case "provider_rate_limit":
|
|
3217
|
+
case "provider_timeout":
|
|
3218
|
+
case "tool_threw":
|
|
3219
|
+
case "budget_timeout":
|
|
3220
|
+
return true;
|
|
3221
|
+
default:
|
|
3222
|
+
return false;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
var REGISTRY_ID_ALIASES = {
|
|
3226
|
+
"claude-code": "claude-acp",
|
|
3227
|
+
"gemini-cli": "gemini",
|
|
3228
|
+
"codex-cli": "codex-acp",
|
|
3229
|
+
copilot: "github-copilot-cli"
|
|
3230
|
+
};
|
|
3231
|
+
function resolveAcpAgentCommand(id, overrides, live) {
|
|
3232
|
+
const ov = overrides?.[id];
|
|
3233
|
+
if (ov && typeof ov.command === "string" && ov.command.length > 0) {
|
|
3234
|
+
const out = {
|
|
3235
|
+
command: ov.command,
|
|
3236
|
+
args: [...ov.args ?? []],
|
|
3237
|
+
role: id
|
|
3238
|
+
};
|
|
3239
|
+
if (ov.env) out.env = ov.env;
|
|
3240
|
+
return out;
|
|
3241
|
+
}
|
|
3242
|
+
const desc = findAgentDescriptor(id);
|
|
3243
|
+
if (desc) {
|
|
3244
|
+
const out = {
|
|
3245
|
+
command: desc.acp.command,
|
|
3246
|
+
args: [...desc.acp.args ?? []],
|
|
3247
|
+
role: id
|
|
3248
|
+
};
|
|
3249
|
+
if (desc.acp.env) out.env = desc.acp.env;
|
|
3250
|
+
return out;
|
|
3251
|
+
}
|
|
3252
|
+
const liveEntry = live?.[id] ?? live?.[REGISTRY_ID_ALIASES[id] ?? ""];
|
|
3253
|
+
if (liveEntry && typeof liveEntry.command === "string" && liveEntry.command.length > 0) {
|
|
3254
|
+
const out = {
|
|
3255
|
+
command: liveEntry.command,
|
|
3256
|
+
args: [...liveEntry.args ?? []],
|
|
3257
|
+
role: id
|
|
3258
|
+
};
|
|
3259
|
+
if (liveEntry.env) out.env = liveEntry.env;
|
|
3260
|
+
return out;
|
|
3261
|
+
}
|
|
3262
|
+
const fromMap = ACP_AGENT_COMMANDS[id];
|
|
3263
|
+
if (fromMap) return fromMap;
|
|
3264
|
+
return null;
|
|
3265
|
+
}
|
|
3266
|
+
async function runOneAcpTask(opts) {
|
|
3267
|
+
const role = opts.role ?? "acp";
|
|
3268
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
3269
|
+
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
3270
|
+
command: opts.command,
|
|
3271
|
+
...opts.args !== void 0 ? { args: opts.args } : {},
|
|
3272
|
+
...opts.env !== void 0 ? { env: opts.env } : {},
|
|
3273
|
+
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
3274
|
+
...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
|
|
3275
|
+
role,
|
|
3276
|
+
timeoutMs,
|
|
3277
|
+
...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
|
|
3278
|
+
...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
|
|
3279
|
+
});
|
|
3280
|
+
try {
|
|
3281
|
+
const budget = new SubagentBudget({
|
|
3282
|
+
timeoutMs,
|
|
3283
|
+
maxIterations: 2e3,
|
|
3284
|
+
maxToolCalls: 5e3
|
|
3285
|
+
});
|
|
3286
|
+
budget.start();
|
|
3287
|
+
const ctx = {
|
|
3288
|
+
subagentId: role,
|
|
3289
|
+
config: { id: role, name: role, role, provider: "acp", prompt: "" },
|
|
3290
|
+
budget,
|
|
3291
|
+
signal: opts.signal ?? new AbortController().signal,
|
|
3292
|
+
bridge: null
|
|
3293
|
+
};
|
|
3294
|
+
const result = await runner({ id: `acp-${role}`, description: opts.task }, ctx);
|
|
3295
|
+
return {
|
|
3296
|
+
result: result.result == null ? "" : String(result.result),
|
|
3297
|
+
iterations: result.iterations,
|
|
3298
|
+
toolCalls: result.toolCalls
|
|
3299
|
+
};
|
|
3300
|
+
} finally {
|
|
3301
|
+
try {
|
|
3302
|
+
await stop();
|
|
3303
|
+
} catch {
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
async function probeAcpAgent(idOrCmd, opts) {
|
|
3308
|
+
const id = typeof idOrCmd === "string" ? idOrCmd : idOrCmd.role ?? idOrCmd.command;
|
|
3309
|
+
const cmd = typeof idOrCmd === "string" ? resolveAcpAgentCommand(idOrCmd, opts?.overrides, opts?.live) : idOrCmd;
|
|
3310
|
+
if (!cmd) return { id, ok: false, ms: 0, error: "unknown agent" };
|
|
3311
|
+
const timeoutMs = opts?.timeoutMs ?? 8e3;
|
|
3312
|
+
const startedAt = Date.now();
|
|
3313
|
+
let session = null;
|
|
3314
|
+
try {
|
|
3315
|
+
session = await ACPSession.start({
|
|
3316
|
+
command: cmd.command,
|
|
3317
|
+
...cmd.args !== void 0 ? { args: cmd.args } : {},
|
|
3318
|
+
...cmd.env !== void 0 ? { env: cmd.env } : {},
|
|
3319
|
+
projectRoot: opts?.projectRoot ?? process.cwd(),
|
|
3320
|
+
// Bounds the `initialize` request: a CLI that spawns but never answers
|
|
3321
|
+
// the handshake fails after this instead of blocking.
|
|
3322
|
+
timeoutMs
|
|
3323
|
+
});
|
|
3324
|
+
const info = session.getAgentInfo();
|
|
3325
|
+
return {
|
|
3326
|
+
id,
|
|
3327
|
+
ok: true,
|
|
3328
|
+
ms: Date.now() - startedAt,
|
|
3329
|
+
...info ? { agentInfo: info } : {}
|
|
3330
|
+
};
|
|
3331
|
+
} catch (err) {
|
|
3332
|
+
return {
|
|
3333
|
+
id,
|
|
3334
|
+
ok: false,
|
|
3335
|
+
ms: Date.now() - startedAt,
|
|
3336
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3337
|
+
};
|
|
3338
|
+
} finally {
|
|
3339
|
+
if (session) {
|
|
3340
|
+
try {
|
|
3341
|
+
await session.close();
|
|
3342
|
+
} catch {
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
export { ACPProtocolHandler, ACPSession, ACPSessionError, ACPSessionStore, ACP_AGENT_COMMANDS, FileServer, FsError, TerminalServer, WRONGSTACK_VERSION, WebSocketClientTransport, WrongStackACPServer, audioContent, defaultPermissionPolicy, disposeACPServerAgentTurn, imageContent, makeACPServerAgentTurn, makeACPSubagentRunner, makeACPSubagentRunnerWithStop, makePermissionPolicy, probeAcpAgent, readOnlyPermissionPolicy, resolveAcpAgentCommand, runOneAcpTask, textContent };
|
|
3349
|
+
//# sourceMappingURL=sdk.js.map
|
|
3350
|
+
//# sourceMappingURL=sdk.js.map
|