@wrongstack/acp 0.257.2 → 0.264.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +60 -88
- package/dist/agent.js +402 -133
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.js +890 -114
- package/dist/client.js.map +1 -1
- package/dist/{index-DPMuJGqv.d.ts → index-BvPqJHhm.d.ts} +46 -14
- package/dist/index.d.ts +497 -3
- package/dist/index.js +1733 -326
- package/dist/index.js.map +1 -1
- package/dist/{stdio-transport-DoKRVjHz.d.ts → stdio-transport-CsFr8JzC.d.ts} +8 -1
- package/dist/tools-registry-BCf8evEG.d.ts +36 -0
- package/dist/wrongstack-acp-agent-Dv-A0bEm.d.ts +310 -0
- package/dist/wrongstack-acp-agent.d.ts +3 -0
- package/dist/wrongstack-acp-agent.js +492 -0
- package/dist/wrongstack-acp-agent.js.map +1 -0
- package/package.json +4 -3
package/dist/client.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { expectDefined, writeErr } from '@wrongstack/core';
|
|
2
|
+
import * as fsp from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
2
5
|
|
|
3
6
|
// src/agent/stdio-transport.ts
|
|
4
7
|
var ClientTransport = class {
|
|
@@ -17,22 +20,30 @@ var ClientTransport = class {
|
|
|
17
20
|
}
|
|
18
21
|
async start() {
|
|
19
22
|
if (this.child) return;
|
|
20
|
-
const [{ spawn }, { buildChildEnv }] = await Promise.all([
|
|
23
|
+
const [{ spawn: spawn2 }, { buildChildEnv }] = await Promise.all([
|
|
21
24
|
import('child_process'),
|
|
22
25
|
import('@wrongstack/core')
|
|
23
26
|
]);
|
|
24
|
-
return new Promise((
|
|
27
|
+
return new Promise((resolve3, reject) => {
|
|
25
28
|
const timeout = setTimeout(() => {
|
|
26
29
|
reject(
|
|
27
30
|
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
28
31
|
);
|
|
29
32
|
}, this.opts.handshakeTimeoutMs);
|
|
30
33
|
try {
|
|
31
|
-
this.child =
|
|
34
|
+
this.child = spawn2(this.opts.command, this.opts.args ?? [], {
|
|
32
35
|
env: { ...buildChildEnv(), ...this.opts.env },
|
|
33
36
|
cwd: this.opts.cwd,
|
|
34
37
|
stdio: ["pipe", "pipe", "pipe"],
|
|
35
|
-
windowsHide: true
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
// On Windows, most ACP-supporting tools (claude, gemini, codex,
|
|
40
|
+
// qwen, copilot) are installed as `.cmd` shims under
|
|
41
|
+
// AppData\Roaming\npm\. Node's spawn won't find them via
|
|
42
|
+
// `shell: false` because the .cmd extension is not in the
|
|
43
|
+
// default PATHEXT lookup. The argv here is always from our
|
|
44
|
+
// own static catalog or from a hardcoded spec, never from
|
|
45
|
+
// user input, so shell-expansion is bounded.
|
|
46
|
+
shell: process.platform === "win32"
|
|
36
47
|
});
|
|
37
48
|
} catch (err) {
|
|
38
49
|
clearTimeout(timeout);
|
|
@@ -41,17 +52,24 @@ var ClientTransport = class {
|
|
|
41
52
|
}
|
|
42
53
|
const child = this.child;
|
|
43
54
|
child.stdout.setEncoding("utf8");
|
|
55
|
+
const onReady = () => {
|
|
56
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
57
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
58
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
resolve3();
|
|
61
|
+
};
|
|
62
|
+
if (this.opts.skipHandshakeMarker) {
|
|
63
|
+
onReady();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
44
66
|
const waitForMarker = (chunk) => {
|
|
45
67
|
this.buffer += chunk;
|
|
46
68
|
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
47
69
|
if (idx !== -1) {
|
|
48
70
|
this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
49
71
|
child.stdout.removeListener("data", waitForMarker);
|
|
50
|
-
|
|
51
|
-
child.stderr.on("data", (c) => this.onChildError(c));
|
|
52
|
-
child.on("close", (code) => this.onChildClose(code));
|
|
53
|
-
clearTimeout(timeout);
|
|
54
|
-
resolve();
|
|
72
|
+
onReady();
|
|
55
73
|
}
|
|
56
74
|
};
|
|
57
75
|
child.stdout.on("data", waitForMarker);
|
|
@@ -67,19 +85,19 @@ var ClientTransport = class {
|
|
|
67
85
|
}
|
|
68
86
|
send(msg) {
|
|
69
87
|
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
70
|
-
return new Promise((
|
|
88
|
+
return new Promise((resolve3, reject) => {
|
|
71
89
|
const line = JSON.stringify(msg) + "\n";
|
|
72
90
|
this.child?.stdin.write(line, "utf8", (err) => {
|
|
73
91
|
if (err) reject(err);
|
|
74
|
-
else
|
|
92
|
+
else resolve3();
|
|
75
93
|
});
|
|
76
94
|
});
|
|
77
95
|
}
|
|
78
96
|
read() {
|
|
79
97
|
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
80
98
|
if (this.closed) return Promise.resolve(null);
|
|
81
|
-
return new Promise((
|
|
82
|
-
this.resolveRead =
|
|
99
|
+
return new Promise((resolve3) => {
|
|
100
|
+
this.resolveRead = resolve3;
|
|
83
101
|
});
|
|
84
102
|
}
|
|
85
103
|
onMessage(handler) {
|
|
@@ -121,9 +139,9 @@ var ClientTransport = class {
|
|
|
121
139
|
}
|
|
122
140
|
dispatch(msg) {
|
|
123
141
|
if (this.resolveRead) {
|
|
124
|
-
const
|
|
142
|
+
const resolve3 = this.resolveRead;
|
|
125
143
|
this.resolveRead = null;
|
|
126
|
-
|
|
144
|
+
resolve3(msg);
|
|
127
145
|
} else {
|
|
128
146
|
this.messageQueue.push(msg);
|
|
129
147
|
}
|
|
@@ -140,32 +158,6 @@ var DEFAULT_OPTIONS = {
|
|
|
140
158
|
pollIntervalMs: 500,
|
|
141
159
|
totalTimeoutMs: 12e4
|
|
142
160
|
};
|
|
143
|
-
function extractTextFromContent(blocks) {
|
|
144
|
-
const parts = [];
|
|
145
|
-
for (const b of blocks) {
|
|
146
|
-
if (b.type === "text") parts.push(b.text);
|
|
147
|
-
else if (b.type === "resource") parts.push(`[resource: ${b.resource.uri}]`);
|
|
148
|
-
else if (b.type === "image") parts.push(`[image: ${b.data.slice(0, 20)}...]`);
|
|
149
|
-
else if (b.type === "progress") {
|
|
150
|
-
if (b.messages?.length) parts.push(b.messages.join("\n"));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return parts.join("\n");
|
|
154
|
-
}
|
|
155
|
-
function parseToolResponse(taskId, subagentId, response) {
|
|
156
|
-
const blocks = response.result.content;
|
|
157
|
-
const text = extractTextFromContent(blocks);
|
|
158
|
-
const isError = response.result.isError || text.toLowerCase().includes("error") || text.toLowerCase().includes("failed");
|
|
159
|
-
return {
|
|
160
|
-
taskId,
|
|
161
|
-
subagentId,
|
|
162
|
-
status: isError ? "failed" : "success",
|
|
163
|
-
result: text,
|
|
164
|
-
iterations: 1,
|
|
165
|
-
toolCalls: 1,
|
|
166
|
-
durationMs: 0
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
161
|
var ToolTranslator = class {
|
|
170
162
|
opts;
|
|
171
163
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -207,12 +199,12 @@ var ToolTranslator = class {
|
|
|
207
199
|
id: callId,
|
|
208
200
|
params: { name, arguments: args }
|
|
209
201
|
});
|
|
210
|
-
return new Promise((
|
|
202
|
+
return new Promise((resolve3, reject) => {
|
|
211
203
|
const timeout = setTimeout(() => {
|
|
212
204
|
this.pending.delete(callId);
|
|
213
205
|
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
214
206
|
}, this.opts.totalTimeoutMs);
|
|
215
|
-
this.pending.set(callId, { resolve, reject, timeout });
|
|
207
|
+
this.pending.set(callId, { resolve: resolve3, reject, timeout });
|
|
216
208
|
});
|
|
217
209
|
}
|
|
218
210
|
cancelAll() {
|
|
@@ -223,97 +215,881 @@ var ToolTranslator = class {
|
|
|
223
215
|
}
|
|
224
216
|
};
|
|
225
217
|
|
|
226
|
-
// src/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
218
|
+
// src/types/acp-v1.ts
|
|
219
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
220
|
+
var FsError = class extends Error {
|
|
221
|
+
code;
|
|
222
|
+
path;
|
|
223
|
+
constructor(code, path3, message) {
|
|
224
|
+
super(message);
|
|
225
|
+
this.name = "FsError";
|
|
226
|
+
this.code = code;
|
|
227
|
+
this.path = path3;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var FileServer = class {
|
|
231
|
+
root;
|
|
232
|
+
timeoutMs;
|
|
233
|
+
constructor(opts) {
|
|
234
|
+
this.root = path.resolve(opts.projectRoot);
|
|
235
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
236
|
+
}
|
|
237
|
+
/** Read a text file. Returns the content as a string. */
|
|
238
|
+
async readTextFile(params) {
|
|
239
|
+
const safe = this.resolveInside(params.path);
|
|
240
|
+
const controller = new AbortController();
|
|
241
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
242
|
+
try {
|
|
243
|
+
const content = await fsp.readFile(safe, {
|
|
244
|
+
encoding: "utf8",
|
|
245
|
+
signal: controller.signal
|
|
246
|
+
});
|
|
247
|
+
return { content };
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (controller.signal.aborted) {
|
|
250
|
+
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
251
|
+
}
|
|
252
|
+
throw mapFsError(err, safe);
|
|
253
|
+
} finally {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** Write a text file. Atomic via write-then-rename. */
|
|
258
|
+
async writeTextFile(params) {
|
|
259
|
+
const safe = this.resolveInside(params.path);
|
|
260
|
+
const controller = new AbortController();
|
|
261
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
262
|
+
const tmp = `${safe}.${randomHex(4)}.tmp`;
|
|
263
|
+
try {
|
|
264
|
+
await fsp.writeFile(tmp, params.content, {
|
|
265
|
+
encoding: "utf8",
|
|
266
|
+
signal: controller.signal
|
|
267
|
+
});
|
|
268
|
+
await fsp.rename(tmp, safe);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
try {
|
|
271
|
+
await fsp.unlink(tmp);
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
if (controller.signal.aborted) {
|
|
275
|
+
throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
|
|
242
276
|
}
|
|
277
|
+
throw mapFsError(err, safe);
|
|
278
|
+
} finally {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Resolve a path; throw `FsError('OUTSIDE_ROOT')` if the result is
|
|
284
|
+
* not under the project root. Symlinks are not followed here — we
|
|
285
|
+
* operate on the textual path. A future hardening pass can
|
|
286
|
+
* `fs.realpath` each access to catch symlink escapes.
|
|
287
|
+
*/
|
|
288
|
+
resolveInside(p) {
|
|
289
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
290
|
+
throw new FsError("INVALID_PATH", p, "path is empty or not a string");
|
|
291
|
+
}
|
|
292
|
+
if (!path.isAbsolute(p)) {
|
|
293
|
+
throw new FsError("INVALID_PATH", p, "path must be absolute (ACP requirement)");
|
|
294
|
+
}
|
|
295
|
+
const resolved = path.resolve(p);
|
|
296
|
+
const rootWithSep = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;
|
|
297
|
+
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
298
|
+
throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
|
|
299
|
+
}
|
|
300
|
+
return resolved;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
function mapFsError(err, p) {
|
|
304
|
+
const code = err?.code;
|
|
305
|
+
if (code === "ENOENT") return new FsError("ENOENT", p, `no such file: ${p}`);
|
|
306
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
307
|
+
return new FsError("EACCES", p, `permission denied: ${p}`);
|
|
308
|
+
}
|
|
309
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
310
|
+
return new FsError("INVALID_PATH", p, msg);
|
|
311
|
+
}
|
|
312
|
+
function randomHex(bytes) {
|
|
313
|
+
let out = "";
|
|
314
|
+
for (let i = 0; i < bytes * 2; i++) {
|
|
315
|
+
out += Math.floor(Math.random() * 16).toString(16);
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/client/permission.ts
|
|
321
|
+
var defaultPermissionPolicy = async (req) => {
|
|
322
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
323
|
+
const ranked = [...req.options].sort((a, b) => {
|
|
324
|
+
const score = (k) => {
|
|
325
|
+
if (k === "allow_always") return 0;
|
|
326
|
+
if (k === "allow_once") return 1;
|
|
327
|
+
if (k === "reject_once") return 2;
|
|
328
|
+
return 3;
|
|
329
|
+
};
|
|
330
|
+
return score(a.kind) - score(b.kind);
|
|
331
|
+
});
|
|
332
|
+
const chosen = ranked[0];
|
|
333
|
+
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
334
|
+
return { outcome: "cancelled" };
|
|
335
|
+
}
|
|
336
|
+
return { outcome: "selected", optionId: chosen.optionId };
|
|
337
|
+
};
|
|
338
|
+
var TerminalServer = class {
|
|
339
|
+
terminals = /* @__PURE__ */ new Map();
|
|
340
|
+
projectRoot;
|
|
341
|
+
commandTimeoutMs;
|
|
342
|
+
outputByteLimit;
|
|
343
|
+
nextId = 1;
|
|
344
|
+
constructor(opts) {
|
|
345
|
+
this.projectRoot = path.resolve(opts.projectRoot);
|
|
346
|
+
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
347
|
+
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
348
|
+
if (opts.signal) {
|
|
349
|
+
opts.signal.addEventListener("abort", () => this.releaseAll());
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
353
|
+
create(params) {
|
|
354
|
+
const id = `term_${this.nextId++}`;
|
|
355
|
+
const cwd = this.resolveCwd(params.cwd);
|
|
356
|
+
const proc = spawn(params.command, params.args ?? [], {
|
|
357
|
+
cwd,
|
|
358
|
+
env: this.buildEnv(params.env),
|
|
359
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
360
|
+
windowsHide: true
|
|
361
|
+
// shell: false on purpose. The terminal server is invoked with
|
|
362
|
+
// the agent's explicit argv; turning on shell-mode would make
|
|
363
|
+
// the command a single shell-parsed string, which breaks
|
|
364
|
+
// Windows cmd quoting for the common case of running node with
|
|
365
|
+
// `-e "<script>"`. If a future feature needs shell features
|
|
366
|
+
// (pipes, redirects), it should be opt-in per-call, not the
|
|
367
|
+
// default.
|
|
243
368
|
});
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
369
|
+
const state = {
|
|
370
|
+
proc,
|
|
371
|
+
cwd,
|
|
372
|
+
command: params.command,
|
|
373
|
+
args: params.args ?? [],
|
|
374
|
+
output: "",
|
|
375
|
+
retainedBytes: 0,
|
|
376
|
+
truncated: false,
|
|
377
|
+
exitStatus: void 0,
|
|
378
|
+
timeoutHandle: null,
|
|
379
|
+
exitPromise: new Promise((resolve3) => {
|
|
380
|
+
proc.on("close", (code, signalName) => {
|
|
381
|
+
if (state.timeoutHandle) {
|
|
382
|
+
clearTimeout(state.timeoutHandle);
|
|
383
|
+
state.timeoutHandle = null;
|
|
384
|
+
}
|
|
385
|
+
const exitStatus = {
|
|
386
|
+
exitCode: typeof code === "number" ? code : null,
|
|
387
|
+
signal: typeof signalName === "string" ? signalName : null
|
|
388
|
+
};
|
|
389
|
+
state.exitStatus = exitStatus;
|
|
390
|
+
resolve3(exitStatus);
|
|
391
|
+
});
|
|
392
|
+
proc.on("error", (err) => {
|
|
393
|
+
if (state.timeoutHandle) {
|
|
394
|
+
clearTimeout(state.timeoutHandle);
|
|
395
|
+
state.timeoutHandle = null;
|
|
396
|
+
}
|
|
397
|
+
const exitStatus = { exitCode: 127, signal: null };
|
|
398
|
+
state.exitStatus = exitStatus;
|
|
399
|
+
state.output += `[spawn error] ${err.message}
|
|
400
|
+
`;
|
|
401
|
+
state.retainedBytes += Buffer.byteLength(state.output, "utf8");
|
|
402
|
+
resolve3(exitStatus);
|
|
403
|
+
});
|
|
404
|
+
})
|
|
405
|
+
};
|
|
406
|
+
const perCallByteLimit = params.outputByteLimit ?? this.outputByteLimit;
|
|
407
|
+
proc.stdout?.setEncoding("utf8");
|
|
408
|
+
proc.stderr?.setEncoding("utf8");
|
|
409
|
+
const onData = (chunk) => {
|
|
410
|
+
state.output += chunk;
|
|
411
|
+
state.retainedBytes = Buffer.byteLength(state.output, "utf8");
|
|
412
|
+
while (state.retainedBytes > perCallByteLimit) {
|
|
413
|
+
const trimmed = state.output.slice(1);
|
|
414
|
+
state.output = trimmed;
|
|
415
|
+
const newBytes = Buffer.byteLength(state.output, "utf8");
|
|
416
|
+
if (newBytes >= state.retainedBytes) {
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
state.retainedBytes = newBytes;
|
|
420
|
+
state.truncated = true;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
proc.stdout?.on("data", onData);
|
|
424
|
+
proc.stderr?.on("data", onData);
|
|
425
|
+
state.timeoutHandle = setTimeout(() => {
|
|
426
|
+
try {
|
|
427
|
+
proc.kill("SIGTERM");
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}, this.commandTimeoutMs);
|
|
431
|
+
this.terminals.set(id, state);
|
|
432
|
+
return { terminalId: id };
|
|
433
|
+
}
|
|
434
|
+
/** Return captured output and (if available) the exit status. */
|
|
435
|
+
output(terminalId) {
|
|
436
|
+
const state = this.terminals.get(terminalId);
|
|
437
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
438
|
+
return {
|
|
439
|
+
output: state.output,
|
|
440
|
+
truncated: state.truncated,
|
|
441
|
+
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
/** Block until the process exits. Resolves with the exit status. */
|
|
445
|
+
async waitForExit(terminalId) {
|
|
446
|
+
const state = this.terminals.get(terminalId);
|
|
447
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
448
|
+
return state.exitPromise;
|
|
449
|
+
}
|
|
450
|
+
/** Kill the process but keep the terminal record (agent can still read output). */
|
|
451
|
+
kill(terminalId) {
|
|
452
|
+
const state = this.terminals.get(terminalId);
|
|
453
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
454
|
+
try {
|
|
455
|
+
state.proc.kill("SIGTERM");
|
|
456
|
+
} catch {
|
|
247
457
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
458
|
+
}
|
|
459
|
+
/** Kill the process if alive and remove the record. */
|
|
460
|
+
release(terminalId) {
|
|
461
|
+
const state = this.terminals.get(terminalId);
|
|
462
|
+
if (!state) return;
|
|
463
|
+
if (state.timeoutHandle) {
|
|
464
|
+
clearTimeout(state.timeoutHandle);
|
|
465
|
+
state.timeoutHandle = null;
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
state.proc.kill("SIGKILL");
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
this.terminals.delete(terminalId);
|
|
472
|
+
}
|
|
473
|
+
/** Kill all active terminals. Used on session close. */
|
|
474
|
+
releaseAll() {
|
|
475
|
+
for (const id of [...this.terminals.keys()]) {
|
|
476
|
+
this.release(id);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
resolveCwd(cwd) {
|
|
480
|
+
if (!cwd) return this.projectRoot;
|
|
481
|
+
const resolved = path.resolve(cwd);
|
|
482
|
+
const rootWithSep = this.projectRoot.endsWith(path.sep) ? this.projectRoot : this.projectRoot + path.sep;
|
|
483
|
+
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
484
|
+
return this.projectRoot;
|
|
485
|
+
}
|
|
486
|
+
return resolved;
|
|
487
|
+
}
|
|
488
|
+
buildEnv(agentEnv) {
|
|
489
|
+
const env = { ...process.env };
|
|
490
|
+
if (process.platform === "win32") {
|
|
491
|
+
if (env.Path !== void 0 && env.PATH === void 0) env.PATH = env.Path;
|
|
492
|
+
if (env.PATHEXT !== void 0 && env.PATHEXT_CASE === void 0) {
|
|
493
|
+
env.PATHEXT_CASE = env.PATHEXT;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (agentEnv) {
|
|
497
|
+
for (const { name, value } of agentEnv) {
|
|
498
|
+
env[name] = value;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return env;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// src/client/acp-session.ts
|
|
506
|
+
var ACPSessionError = class extends Error {
|
|
507
|
+
kind;
|
|
508
|
+
cause;
|
|
509
|
+
constructor(kind, message, cause) {
|
|
510
|
+
super(message);
|
|
511
|
+
this.name = "ACPSessionError";
|
|
512
|
+
this.kind = kind;
|
|
513
|
+
this.cause = cause;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
function isJsonRpcError(v) {
|
|
517
|
+
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
518
|
+
}
|
|
519
|
+
var ACPSession = class _ACPSession {
|
|
520
|
+
transport;
|
|
521
|
+
fileServer;
|
|
522
|
+
terminalServer;
|
|
523
|
+
permissionPolicy;
|
|
524
|
+
timeoutMs;
|
|
525
|
+
opts;
|
|
526
|
+
state = "init";
|
|
527
|
+
sessionId = null;
|
|
528
|
+
/** Pending outbound requests (initialize, session/new, session/prompt, etc). */
|
|
529
|
+
pending = /* @__PURE__ */ new Map();
|
|
530
|
+
nextId = 1;
|
|
531
|
+
/** True after close() has been called. */
|
|
532
|
+
closed = false;
|
|
533
|
+
constructor(opts, transport) {
|
|
534
|
+
this.opts = opts;
|
|
535
|
+
this.transport = transport;
|
|
536
|
+
this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
537
|
+
const fsOpts = {
|
|
538
|
+
projectRoot: opts.projectRoot
|
|
539
|
+
};
|
|
540
|
+
if (opts.fsTimeoutMs !== void 0) fsOpts.timeoutMs = opts.fsTimeoutMs;
|
|
541
|
+
this.fileServer = new FileServer(fsOpts);
|
|
542
|
+
const termOpts = {
|
|
543
|
+
projectRoot: opts.projectRoot
|
|
544
|
+
};
|
|
545
|
+
if (opts.terminalTimeoutMs !== void 0) {
|
|
546
|
+
termOpts.commandTimeoutMs = opts.terminalTimeoutMs;
|
|
547
|
+
}
|
|
548
|
+
if (opts.terminalOutputByteLimit !== void 0) {
|
|
549
|
+
termOpts.outputByteLimit = opts.terminalOutputByteLimit;
|
|
550
|
+
}
|
|
551
|
+
this.terminalServer = new TerminalServer(termOpts);
|
|
552
|
+
this.permissionPolicy = opts.permissionPolicy ?? defaultPermissionPolicy;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Spawn the child, run the initialize handshake, install the
|
|
556
|
+
* message dispatch, and return a ready session.
|
|
557
|
+
*/
|
|
558
|
+
static async start(opts) {
|
|
559
|
+
const transportOpts = {
|
|
560
|
+
command: opts.command,
|
|
561
|
+
args: opts.args ? [...opts.args] : [],
|
|
562
|
+
handshakeTimeoutMs: 3e4,
|
|
563
|
+
// ACPSession is the v1 CLIENT side: it speaks to external agents
|
|
564
|
+
// (Claude Code, Gemini CLI, …) that do NOT emit a `[wstack-acp]\n`
|
|
565
|
+
// startup marker. The transport should treat the child as ready
|
|
566
|
+
// as soon as the process is spawned and stdout is flowing.
|
|
567
|
+
skipHandshakeMarker: true
|
|
568
|
+
};
|
|
569
|
+
if (opts.env !== void 0) transportOpts.env = opts.env;
|
|
570
|
+
if (opts.cwd !== void 0) transportOpts.cwd = opts.cwd;
|
|
571
|
+
const transport = new ClientTransport(transportOpts);
|
|
572
|
+
try {
|
|
573
|
+
await transport.start();
|
|
574
|
+
} catch (err) {
|
|
575
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
576
|
+
throw new ACPSessionError("spawn_failed", `failed to spawn ${opts.command}: ${msg}`, err);
|
|
577
|
+
}
|
|
578
|
+
const session = new _ACPSession(opts, transport);
|
|
579
|
+
transport.onMessage((msg) => session.handleMessage(msg));
|
|
580
|
+
try {
|
|
581
|
+
await session.initialize();
|
|
582
|
+
} catch (err) {
|
|
583
|
+
try {
|
|
584
|
+
transport.stop();
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
589
|
+
return session;
|
|
590
|
+
}
|
|
591
|
+
async initialize() {
|
|
592
|
+
const id = this.allocId();
|
|
593
|
+
const result = await this.sendRequest(id, "initialize", {
|
|
594
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
595
|
+
clientCapabilities: {
|
|
596
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
597
|
+
terminal: true,
|
|
598
|
+
promptCapabilities: { image: false, audio: false, embeddedContext: true }
|
|
599
|
+
},
|
|
600
|
+
clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.263.0" }
|
|
251
601
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
602
|
+
if (isJsonRpcError(result)) {
|
|
603
|
+
throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
|
|
604
|
+
}
|
|
605
|
+
if (typeof result !== "object" || result === null || typeof result.protocolVersion !== "number") {
|
|
606
|
+
throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
|
|
607
|
+
}
|
|
608
|
+
const r = result;
|
|
609
|
+
if (r.protocolVersion !== ACP_PROTOCOL_VERSION) {
|
|
610
|
+
throw new ACPSessionError(
|
|
611
|
+
"unsupported_capability",
|
|
612
|
+
`agent speaks protocolVersion=${r.protocolVersion}, client speaks ${ACP_PROTOCOL_VERSION}`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
this.state = "ready";
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Run one prompt turn. Creates a session if needed, sends the
|
|
619
|
+
* prompt, streams session/update notifications, and resolves with
|
|
620
|
+
* the agent's response.
|
|
621
|
+
*
|
|
622
|
+
* Cancellation: if `signal` aborts mid-prompt, we send
|
|
623
|
+
* `session/cancel` (a notification per spec) and keep accepting
|
|
624
|
+
* updates until the agent returns with `stopReason: 'cancelled'`.
|
|
625
|
+
* The result is the same shape as a normal turn, with
|
|
626
|
+
* `stopReason === 'cancelled'`.
|
|
627
|
+
*/
|
|
628
|
+
async prompt(text, signal) {
|
|
629
|
+
if (this.closed) {
|
|
630
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
631
|
+
}
|
|
632
|
+
if (this.state !== "ready" && this.state !== "done") {
|
|
633
|
+
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
634
|
+
}
|
|
635
|
+
if (signal.aborted) {
|
|
636
|
+
return { text: "", stopReason: "cancelled", hasText: false };
|
|
637
|
+
}
|
|
638
|
+
if (!this.sessionId) {
|
|
639
|
+
await this.createSession();
|
|
640
|
+
}
|
|
641
|
+
this.resetScratch();
|
|
642
|
+
const promptId = this.allocId();
|
|
643
|
+
const turnPromise = this.sendRequest(
|
|
644
|
+
promptId,
|
|
645
|
+
"session/prompt",
|
|
646
|
+
{
|
|
647
|
+
sessionId: this.sessionId,
|
|
648
|
+
prompt: [textContent(text)]
|
|
649
|
+
},
|
|
650
|
+
this.timeoutMs
|
|
651
|
+
);
|
|
652
|
+
let cancelled = false;
|
|
653
|
+
const onAbort = () => {
|
|
654
|
+
cancelled = true;
|
|
655
|
+
this.transport.send({ method: "session/cancel", params: { sessionId: this.sessionId } }).catch(() => {
|
|
656
|
+
});
|
|
657
|
+
};
|
|
658
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
659
|
+
this.state = "prompting";
|
|
660
|
+
let response;
|
|
661
|
+
try {
|
|
662
|
+
response = await turnPromise;
|
|
663
|
+
} catch (err) {
|
|
664
|
+
this.state = "done";
|
|
665
|
+
signal.removeEventListener("abort", onAbort);
|
|
666
|
+
if (cancelled || signal.aborted) {
|
|
667
|
+
throw new ACPSessionError("aborted", "prompt was aborted by the parent");
|
|
668
|
+
}
|
|
669
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
670
|
+
throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
|
|
671
|
+
} finally {
|
|
672
|
+
signal.removeEventListener("abort", onAbort);
|
|
673
|
+
}
|
|
674
|
+
this.state = "done";
|
|
675
|
+
if (isJsonRpcError(response)) {
|
|
676
|
+
throw new ACPSessionError("prompt_failed", `agent error: ${response.message}`, response);
|
|
677
|
+
}
|
|
678
|
+
const stopReason = response.stopReason ?? "end_turn";
|
|
679
|
+
const finalText = this.scratch.text;
|
|
680
|
+
return {
|
|
681
|
+
text: finalText,
|
|
682
|
+
stopReason,
|
|
683
|
+
hasText: finalText.length > 0,
|
|
684
|
+
usage: this.scratch.usage,
|
|
685
|
+
plan: this.scratch.plan
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
async createSession() {
|
|
689
|
+
const id = this.allocId();
|
|
690
|
+
const result = await this.sendRequest(id, "session/new", {
|
|
691
|
+
cwd: this.opts.cwd ?? this.opts.projectRoot,
|
|
692
|
+
mcpServers: []
|
|
258
693
|
});
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
694
|
+
if (isJsonRpcError(result)) {
|
|
695
|
+
throw new ACPSessionError(
|
|
696
|
+
"session_create_failed",
|
|
697
|
+
`session/new failed: ${result.message}`,
|
|
698
|
+
result
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
const sessionId = result.sessionId;
|
|
702
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
703
|
+
throw new ACPSessionError(
|
|
704
|
+
"protocol_error",
|
|
705
|
+
"session/new returned no sessionId",
|
|
706
|
+
result
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
this.sessionId = sessionId;
|
|
710
|
+
}
|
|
711
|
+
/** Tear down the session and kill the child process. */
|
|
712
|
+
async close() {
|
|
713
|
+
if (this.closed) return;
|
|
714
|
+
this.closed = true;
|
|
715
|
+
this.state = "closed";
|
|
716
|
+
this.terminalServer.releaseAll();
|
|
717
|
+
for (const [, p] of this.pending) {
|
|
718
|
+
clearTimeout(p.timeoutHandle);
|
|
719
|
+
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
720
|
+
}
|
|
721
|
+
this.pending.clear();
|
|
722
|
+
try {
|
|
723
|
+
this.transport.stop();
|
|
724
|
+
} catch {
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// ────────────────────────────────────────────────────────────────────
|
|
728
|
+
// Wire layer
|
|
729
|
+
// ────────────────────────────────────────────────────────────────────
|
|
730
|
+
allocId() {
|
|
731
|
+
return this.nextId++;
|
|
732
|
+
}
|
|
733
|
+
async sendRequest(id, method, params, timeoutMs) {
|
|
734
|
+
return new Promise((resolve3, reject) => {
|
|
735
|
+
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
736
|
+
const handle = setTimeout(() => {
|
|
737
|
+
this.pending.delete(id);
|
|
738
|
+
reject(
|
|
739
|
+
new ACPSessionError(
|
|
740
|
+
"protocol_error",
|
|
741
|
+
`${method} timed out after ${effectiveTimeout}ms`
|
|
742
|
+
)
|
|
743
|
+
);
|
|
744
|
+
}, effectiveTimeout);
|
|
745
|
+
this.pending.set(id, {
|
|
746
|
+
method,
|
|
747
|
+
resolve: resolve3,
|
|
748
|
+
reject,
|
|
749
|
+
timeoutMs: effectiveTimeout,
|
|
750
|
+
timeoutHandle: handle
|
|
272
751
|
});
|
|
273
|
-
|
|
274
|
-
clearTimeout(
|
|
275
|
-
|
|
752
|
+
this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
|
753
|
+
clearTimeout(handle);
|
|
754
|
+
this.pending.delete(id);
|
|
755
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
756
|
+
reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
handleMessage(msg) {
|
|
761
|
+
if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
|
|
762
|
+
const pending = this.pending.get(msg.id);
|
|
763
|
+
if (!pending) return;
|
|
764
|
+
clearTimeout(pending.timeoutHandle);
|
|
765
|
+
this.pending.delete(msg.id);
|
|
766
|
+
if (msg.error !== void 0) {
|
|
767
|
+
pending.reject(new Error(msg.error.message ?? "unknown JSON-RPC error"));
|
|
768
|
+
} else {
|
|
769
|
+
pending.resolve(msg.result);
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (msg.method === "session/update") {
|
|
774
|
+
this.handleUpdate(msg);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (msg.method === "session/request_permission") {
|
|
778
|
+
void this.handlePermissionRequest(msg);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
|
|
782
|
+
void this.handleFsRequest(msg);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (msg.method && msg.method.startsWith("terminal/")) {
|
|
786
|
+
void this.handleTerminalRequest(msg);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (msg.method) {
|
|
790
|
+
console.warn(`[acp-session] unhandled method: ${msg.method}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
handleUpdate(msg) {
|
|
794
|
+
const update = msg.params?.update;
|
|
795
|
+
if (typeof update !== "object" || update === null) return;
|
|
796
|
+
const u = update;
|
|
797
|
+
switch (u.sessionUpdate) {
|
|
798
|
+
case "agent_message_chunk": {
|
|
799
|
+
const text = extractText(u.content);
|
|
800
|
+
if (text) this.accumulatedText(text);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
case "thought_chunk":
|
|
804
|
+
return;
|
|
805
|
+
case "tool_call":
|
|
806
|
+
case "tool_call_update":
|
|
807
|
+
return;
|
|
808
|
+
case "plan":
|
|
809
|
+
if (Array.isArray(u.entries)) {
|
|
810
|
+
this.accumulatedPlan(u.entries);
|
|
811
|
+
}
|
|
812
|
+
return;
|
|
813
|
+
case "usage_update":
|
|
814
|
+
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
815
|
+
this.accumulatedUsage({
|
|
816
|
+
used: u.used,
|
|
817
|
+
size: u.size,
|
|
818
|
+
...typeof u.cost === "object" && u.cost !== null ? {
|
|
819
|
+
cost: u.cost
|
|
820
|
+
} : {}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
case "available_commands_update":
|
|
825
|
+
case "current_mode_update":
|
|
826
|
+
case "config_option_update":
|
|
827
|
+
case "session_info_update":
|
|
828
|
+
case "user_message_chunk":
|
|
829
|
+
return;
|
|
830
|
+
default:
|
|
831
|
+
console.warn(`[acp-session] unhandled sessionUpdate: ${u.sessionUpdate}`);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// Per-prompt scratch state. Reset at the start of each prompt() and
|
|
836
|
+
// read at the end to assemble the ACPSessionRunResult. The stream
|
|
837
|
+
// pump writes to it via the three `accumulated*` helpers below.
|
|
838
|
+
scratch = { text: "" };
|
|
839
|
+
accumulatedText(chunk) {
|
|
840
|
+
this.scratch.text += chunk;
|
|
841
|
+
}
|
|
842
|
+
accumulatedPlan(entries) {
|
|
843
|
+
this.scratch.plan = entries;
|
|
844
|
+
}
|
|
845
|
+
accumulatedUsage(u) {
|
|
846
|
+
this.scratch.usage = u;
|
|
847
|
+
}
|
|
848
|
+
resetScratch() {
|
|
849
|
+
this.scratch = { text: "" };
|
|
850
|
+
}
|
|
851
|
+
async handlePermissionRequest(msg) {
|
|
852
|
+
const id = msg.id;
|
|
853
|
+
if (id === void 0) return;
|
|
854
|
+
const params = msg.params;
|
|
855
|
+
const toolCall = params?.toolCall;
|
|
856
|
+
const options = Array.isArray(params?.options) ? params.options : [];
|
|
857
|
+
if (!toolCall) {
|
|
858
|
+
await this.transport.send({
|
|
859
|
+
id,
|
|
860
|
+
method: "session/request_permission",
|
|
861
|
+
error: { code: -32602, message: "toolCall is required" }
|
|
276
862
|
});
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const policyAbort = new AbortController();
|
|
866
|
+
const outcome = await this.permissionPolicy({
|
|
867
|
+
toolCall,
|
|
868
|
+
options,
|
|
869
|
+
signal: policyAbort.signal
|
|
277
870
|
});
|
|
871
|
+
await this.transport.send({
|
|
872
|
+
id,
|
|
873
|
+
method: "session/request_permission",
|
|
874
|
+
result: { outcome }
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
async handleFsRequest(msg) {
|
|
878
|
+
const id = msg.id;
|
|
879
|
+
if (id === void 0) return;
|
|
880
|
+
const params = msg.params;
|
|
881
|
+
if (!params?.path) {
|
|
882
|
+
await this.transport.send({
|
|
883
|
+
id,
|
|
884
|
+
method: msg.method,
|
|
885
|
+
error: { code: -32602, message: "path is required" }
|
|
886
|
+
});
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
if (msg.method === "fs/read_text_file") {
|
|
891
|
+
const result = await this.fileServer.readTextFile({
|
|
892
|
+
sessionId: params.sessionId ?? "",
|
|
893
|
+
path: params.path
|
|
894
|
+
});
|
|
895
|
+
await this.transport.send({ id, method: msg.method, result });
|
|
896
|
+
} else {
|
|
897
|
+
await this.fileServer.writeTextFile({
|
|
898
|
+
sessionId: params.sessionId ?? "",
|
|
899
|
+
path: params.path,
|
|
900
|
+
content: params.content ?? ""
|
|
901
|
+
});
|
|
902
|
+
await this.transport.send({ id, method: msg.method, result: {} });
|
|
903
|
+
}
|
|
904
|
+
} catch (err) {
|
|
905
|
+
const code = err instanceof FsError ? -32602 : -32603;
|
|
906
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
907
|
+
await this.transport.send({ id, method: msg.method, error: { code, message } });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async handleTerminalRequest(msg) {
|
|
911
|
+
const id = msg.id;
|
|
912
|
+
if (id === void 0) return;
|
|
913
|
+
const params = msg.params ?? {};
|
|
278
914
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
915
|
+
switch (msg.method) {
|
|
916
|
+
case "terminal/create": {
|
|
917
|
+
const createOpts = {
|
|
918
|
+
sessionId: String(params.sessionId ?? ""),
|
|
919
|
+
command: String(params.command ?? ""),
|
|
920
|
+
args: Array.isArray(params.args) ? params.args : []
|
|
921
|
+
};
|
|
922
|
+
if (Array.isArray(params.env)) {
|
|
923
|
+
createOpts.env = params.env;
|
|
924
|
+
}
|
|
925
|
+
if (typeof params.cwd === "string") {
|
|
926
|
+
createOpts.cwd = params.cwd;
|
|
927
|
+
}
|
|
928
|
+
if (typeof params.outputByteLimit === "number") {
|
|
929
|
+
createOpts.outputByteLimit = params.outputByteLimit;
|
|
930
|
+
}
|
|
931
|
+
const result = this.terminalServer.create(createOpts);
|
|
932
|
+
await this.transport.send({ id, method: msg.method, result });
|
|
933
|
+
return;
|
|
285
934
|
}
|
|
935
|
+
case "terminal/output": {
|
|
936
|
+
const terminalId = String(params.terminalId ?? "");
|
|
937
|
+
const out = this.terminalServer.output(terminalId);
|
|
938
|
+
await this.transport.send({ id, method: msg.method, result: out });
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
case "terminal/wait_for_exit": {
|
|
942
|
+
const terminalId = String(params.terminalId ?? "");
|
|
943
|
+
const exit = await this.terminalServer.waitForExit(terminalId);
|
|
944
|
+
await this.transport.send({ id, method: msg.method, result: exit });
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
case "terminal/kill": {
|
|
948
|
+
const terminalId = String(params.terminalId ?? "");
|
|
949
|
+
this.terminalServer.kill(terminalId);
|
|
950
|
+
await this.transport.send({ id, method: msg.method, result: {} });
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
case "terminal/release": {
|
|
954
|
+
const terminalId = String(params.terminalId ?? "");
|
|
955
|
+
this.terminalServer.release(terminalId);
|
|
956
|
+
await this.transport.send({ id, method: msg.method, result: {} });
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
default:
|
|
960
|
+
await this.transport.send({
|
|
961
|
+
id,
|
|
962
|
+
method: msg.method,
|
|
963
|
+
error: { code: -32601, message: `unknown method: ${msg.method}` }
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
} catch (err) {
|
|
967
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
968
|
+
await this.transport.send({
|
|
969
|
+
id,
|
|
970
|
+
method: msg.method,
|
|
971
|
+
error: { code: -32603, message }
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
function textContent(text) {
|
|
977
|
+
return { type: "text", text };
|
|
978
|
+
}
|
|
979
|
+
function extractText(block) {
|
|
980
|
+
if (typeof block !== "object" || block === null) return "";
|
|
981
|
+
const b = block;
|
|
982
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
983
|
+
return "";
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/integration/acp-subagent-runner.ts
|
|
987
|
+
async function makeACPSubagentRunner(options) {
|
|
988
|
+
const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
|
|
989
|
+
const wrappedRunner = async (task, ctx) => {
|
|
990
|
+
try {
|
|
991
|
+
return await runner(task, ctx);
|
|
992
|
+
} finally {
|
|
993
|
+
stop();
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
return wrappedRunner;
|
|
997
|
+
}
|
|
998
|
+
async function makeACPSubagentRunnerWithStop(options) {
|
|
999
|
+
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
1000
|
+
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
1001
|
+
const runner = async (task, ctx) => {
|
|
1002
|
+
let session = null;
|
|
1003
|
+
try {
|
|
1004
|
+
session = await ACPSession.start({
|
|
1005
|
+
command: options.command,
|
|
1006
|
+
...options.args !== void 0 ? { args: options.args } : {},
|
|
1007
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
1008
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1009
|
+
projectRoot,
|
|
1010
|
+
timeoutMs,
|
|
1011
|
+
role: options.role
|
|
286
1012
|
});
|
|
287
|
-
toolResult = await resultPromise;
|
|
288
1013
|
} catch (err) {
|
|
289
|
-
|
|
1014
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
const result = await session.prompt(task.description, ctx.signal);
|
|
290
1018
|
return {
|
|
291
|
-
result:
|
|
292
|
-
iterations:
|
|
1019
|
+
result: result.text,
|
|
1020
|
+
iterations: 1,
|
|
293
1021
|
toolCalls: 0
|
|
294
1022
|
};
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
if (err instanceof ACPSessionError && err.kind === "aborted") {
|
|
1025
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1026
|
+
}
|
|
1027
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1028
|
+
} finally {
|
|
1029
|
+
try {
|
|
1030
|
+
await session.close();
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
295
1033
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
1034
|
+
};
|
|
1035
|
+
const stop = () => {
|
|
1036
|
+
};
|
|
1037
|
+
return { runner, stop };
|
|
1038
|
+
}
|
|
1039
|
+
function acpErrorToSubagentError(err, subagentId) {
|
|
1040
|
+
if (err instanceof ACPSessionError) {
|
|
1041
|
+
const kind = mapACPKind(err.kind);
|
|
300
1042
|
return {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1043
|
+
kind,
|
|
1044
|
+
message: `${subagentId}: ${err.message}`,
|
|
1045
|
+
retryable: isRetryable(kind),
|
|
1046
|
+
cause: {
|
|
1047
|
+
name: err.name,
|
|
1048
|
+
message: err.message,
|
|
1049
|
+
...err.stack !== void 0 ? { stack: err.stack } : {}
|
|
1050
|
+
}
|
|
304
1051
|
};
|
|
1052
|
+
}
|
|
1053
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1054
|
+
return {
|
|
1055
|
+
kind: "bridge_failed",
|
|
1056
|
+
message: `${subagentId}: ${message}`,
|
|
1057
|
+
retryable: false,
|
|
1058
|
+
cause: {
|
|
1059
|
+
name: err instanceof Error ? err.name : "Error",
|
|
1060
|
+
message,
|
|
1061
|
+
...err instanceof Error && err.stack !== void 0 ? { stack: err.stack } : {}
|
|
1062
|
+
}
|
|
305
1063
|
};
|
|
306
|
-
return runner;
|
|
307
1064
|
}
|
|
308
|
-
function
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
1065
|
+
function mapACPKind(acpKind) {
|
|
1066
|
+
switch (acpKind) {
|
|
1067
|
+
case "spawn_failed":
|
|
1068
|
+
case "init_failed":
|
|
1069
|
+
case "session_create_failed":
|
|
1070
|
+
case "agent_died":
|
|
1071
|
+
case "protocol_error":
|
|
1072
|
+
return "bridge_failed";
|
|
1073
|
+
case "prompt_failed":
|
|
1074
|
+
return "tool_failed";
|
|
1075
|
+
case "aborted":
|
|
1076
|
+
return "aborted_by_parent";
|
|
1077
|
+
case "closed":
|
|
1078
|
+
case "unsupported_capability":
|
|
1079
|
+
return "unknown";
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function isRetryable(kind) {
|
|
1083
|
+
switch (kind) {
|
|
1084
|
+
case "provider_5xx":
|
|
1085
|
+
case "provider_rate_limit":
|
|
1086
|
+
case "provider_timeout":
|
|
1087
|
+
case "tool_threw":
|
|
1088
|
+
case "budget_timeout":
|
|
1089
|
+
return true;
|
|
1090
|
+
default:
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
317
1093
|
}
|
|
318
1094
|
|
|
319
1095
|
export { ClientTransport, ToolTranslator, makeACPSubagentRunner };
|