@wrongstack/acp 0.260.0 → 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/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { writeErr, expectDefined } from '@wrongstack/core';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
+
import * as fsp from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { SubagentBudget } from '@wrongstack/core/coordination';
|
|
3
7
|
|
|
4
8
|
// src/agent/stdio-transport.ts
|
|
5
9
|
var StdioTransport = class {
|
|
@@ -23,9 +27,9 @@ var StdioTransport = class {
|
|
|
23
27
|
}
|
|
24
28
|
send(msg) {
|
|
25
29
|
if (this.closed) return Promise.resolve();
|
|
26
|
-
return new Promise((
|
|
30
|
+
return new Promise((resolve3) => {
|
|
27
31
|
const line = JSON.stringify(msg) + "\n";
|
|
28
|
-
this.stdout.write(line, "utf8", () =>
|
|
32
|
+
this.stdout.write(line, "utf8", () => resolve3());
|
|
29
33
|
});
|
|
30
34
|
}
|
|
31
35
|
sendRaw(chunk) {
|
|
@@ -34,8 +38,8 @@ var StdioTransport = class {
|
|
|
34
38
|
read() {
|
|
35
39
|
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
36
40
|
if (this.closed) return Promise.resolve(null);
|
|
37
|
-
return new Promise((
|
|
38
|
-
this.resolveRead =
|
|
41
|
+
return new Promise((resolve3) => {
|
|
42
|
+
this.resolveRead = resolve3;
|
|
39
43
|
});
|
|
40
44
|
}
|
|
41
45
|
onMessage(handler) {
|
|
@@ -64,9 +68,9 @@ var StdioTransport = class {
|
|
|
64
68
|
}
|
|
65
69
|
dispatch(msg) {
|
|
66
70
|
if (this.resolveRead) {
|
|
67
|
-
const
|
|
71
|
+
const resolve3 = this.resolveRead;
|
|
68
72
|
this.resolveRead = null;
|
|
69
|
-
|
|
73
|
+
resolve3(msg);
|
|
70
74
|
} else {
|
|
71
75
|
this.messageQueue.push(msg);
|
|
72
76
|
}
|
|
@@ -106,22 +110,30 @@ var ClientTransport = class {
|
|
|
106
110
|
}
|
|
107
111
|
async start() {
|
|
108
112
|
if (this.child) return;
|
|
109
|
-
const [{ spawn }, { buildChildEnv }] = await Promise.all([
|
|
113
|
+
const [{ spawn: spawn3 }, { buildChildEnv }] = await Promise.all([
|
|
110
114
|
import('child_process'),
|
|
111
115
|
import('@wrongstack/core')
|
|
112
116
|
]);
|
|
113
|
-
return new Promise((
|
|
117
|
+
return new Promise((resolve3, reject) => {
|
|
114
118
|
const timeout = setTimeout(() => {
|
|
115
119
|
reject(
|
|
116
120
|
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
117
121
|
);
|
|
118
122
|
}, this.opts.handshakeTimeoutMs);
|
|
119
123
|
try {
|
|
120
|
-
this.child =
|
|
124
|
+
this.child = spawn3(this.opts.command, this.opts.args ?? [], {
|
|
121
125
|
env: { ...buildChildEnv(), ...this.opts.env },
|
|
122
126
|
cwd: this.opts.cwd,
|
|
123
127
|
stdio: ["pipe", "pipe", "pipe"],
|
|
124
|
-
windowsHide: true
|
|
128
|
+
windowsHide: true,
|
|
129
|
+
// On Windows, most ACP-supporting tools (claude, gemini, codex,
|
|
130
|
+
// qwen, copilot) are installed as `.cmd` shims under
|
|
131
|
+
// AppData\Roaming\npm\. Node's spawn won't find them via
|
|
132
|
+
// `shell: false` because the .cmd extension is not in the
|
|
133
|
+
// default PATHEXT lookup. The argv here is always from our
|
|
134
|
+
// own static catalog or from a hardcoded spec, never from
|
|
135
|
+
// user input, so shell-expansion is bounded.
|
|
136
|
+
shell: process.platform === "win32"
|
|
125
137
|
});
|
|
126
138
|
} catch (err) {
|
|
127
139
|
clearTimeout(timeout);
|
|
@@ -130,17 +142,24 @@ var ClientTransport = class {
|
|
|
130
142
|
}
|
|
131
143
|
const child = this.child;
|
|
132
144
|
child.stdout.setEncoding("utf8");
|
|
145
|
+
const onReady = () => {
|
|
146
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
147
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
148
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
resolve3();
|
|
151
|
+
};
|
|
152
|
+
if (this.opts.skipHandshakeMarker) {
|
|
153
|
+
onReady();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
133
156
|
const waitForMarker = (chunk) => {
|
|
134
157
|
this.buffer += chunk;
|
|
135
158
|
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
136
159
|
if (idx !== -1) {
|
|
137
160
|
this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
138
161
|
child.stdout.removeListener("data", waitForMarker);
|
|
139
|
-
|
|
140
|
-
child.stderr.on("data", (c) => this.onChildError(c));
|
|
141
|
-
child.on("close", (code) => this.onChildClose(code));
|
|
142
|
-
clearTimeout(timeout);
|
|
143
|
-
resolve();
|
|
162
|
+
onReady();
|
|
144
163
|
}
|
|
145
164
|
};
|
|
146
165
|
child.stdout.on("data", waitForMarker);
|
|
@@ -156,19 +175,19 @@ var ClientTransport = class {
|
|
|
156
175
|
}
|
|
157
176
|
send(msg) {
|
|
158
177
|
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
159
|
-
return new Promise((
|
|
178
|
+
return new Promise((resolve3, reject) => {
|
|
160
179
|
const line = JSON.stringify(msg) + "\n";
|
|
161
180
|
this.child?.stdin.write(line, "utf8", (err) => {
|
|
162
181
|
if (err) reject(err);
|
|
163
|
-
else
|
|
182
|
+
else resolve3();
|
|
164
183
|
});
|
|
165
184
|
});
|
|
166
185
|
}
|
|
167
186
|
read() {
|
|
168
187
|
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
169
188
|
if (this.closed) return Promise.resolve(null);
|
|
170
|
-
return new Promise((
|
|
171
|
-
this.resolveRead =
|
|
189
|
+
return new Promise((resolve3) => {
|
|
190
|
+
this.resolveRead = resolve3;
|
|
172
191
|
});
|
|
173
192
|
}
|
|
174
193
|
onMessage(handler) {
|
|
@@ -210,9 +229,9 @@ var ClientTransport = class {
|
|
|
210
229
|
}
|
|
211
230
|
dispatch(msg) {
|
|
212
231
|
if (this.resolveRead) {
|
|
213
|
-
const
|
|
232
|
+
const resolve3 = this.resolveRead;
|
|
214
233
|
this.resolveRead = null;
|
|
215
|
-
|
|
234
|
+
resolve3(msg);
|
|
216
235
|
} else {
|
|
217
236
|
this.messageQueue.push(msg);
|
|
218
237
|
}
|
|
@@ -339,182 +358,366 @@ function toolToPriority(tool) {
|
|
|
339
358
|
return "low";
|
|
340
359
|
}
|
|
341
360
|
|
|
361
|
+
// src/types/acp-v1.ts
|
|
362
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
363
|
+
|
|
342
364
|
// src/agent/protocol-handler.ts
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
365
|
+
function toWire(msg) {
|
|
366
|
+
return msg;
|
|
367
|
+
}
|
|
368
|
+
var WRONGSTACK_VERSION = "0.263.0";
|
|
369
|
+
var DEFAULT_MODE_ID = "code";
|
|
370
|
+
var DEFAULT_MODES = [
|
|
371
|
+
{
|
|
372
|
+
id: DEFAULT_MODE_ID,
|
|
373
|
+
name: "Code",
|
|
374
|
+
description: "Default agent mode for code-generation tasks."
|
|
375
|
+
}
|
|
349
376
|
];
|
|
350
377
|
var ACPProtocolHandler = class {
|
|
351
|
-
constructor(transport, registry, context) {
|
|
352
|
-
this.transport = transport;
|
|
353
|
-
this.registry = registry;
|
|
354
|
-
this.context = context;
|
|
355
|
-
}
|
|
356
378
|
transport;
|
|
357
|
-
|
|
358
|
-
|
|
379
|
+
defaultCwd;
|
|
380
|
+
runTurn;
|
|
381
|
+
onSessionNew;
|
|
382
|
+
modes;
|
|
383
|
+
configOptions;
|
|
384
|
+
agentName;
|
|
359
385
|
initialized = false;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
386
|
+
sessions = /* @__PURE__ */ new Map();
|
|
387
|
+
nextId = 1;
|
|
388
|
+
constructor(opts) {
|
|
389
|
+
this.transport = opts.transport;
|
|
390
|
+
this.defaultCwd = opts.defaultCwd;
|
|
391
|
+
this.runTurn = opts.runTurn;
|
|
392
|
+
this.onSessionNew = opts.onSessionNew ?? (() => {
|
|
368
393
|
});
|
|
394
|
+
this.modes = opts.modes ?? DEFAULT_MODES;
|
|
395
|
+
this.configOptions = opts.configOptions ?? [];
|
|
396
|
+
this.agentName = opts.agentName ?? "wrongstack";
|
|
369
397
|
}
|
|
370
|
-
/**
|
|
398
|
+
/**
|
|
399
|
+
* Process one inbound message. Returns true if this was a terminal
|
|
400
|
+
* message (rare; reserved for future use by the server's own
|
|
401
|
+
* shutdown signal).
|
|
402
|
+
*/
|
|
371
403
|
async handleMessage(msg) {
|
|
372
|
-
if (msg
|
|
373
|
-
|
|
404
|
+
if (typeof msg !== "object" || msg === null) return false;
|
|
405
|
+
const m = msg;
|
|
406
|
+
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
407
|
+
return false;
|
|
374
408
|
}
|
|
375
|
-
|
|
409
|
+
if (m.id !== void 0 && typeof m.method === "string") {
|
|
410
|
+
return this.handleRequest(m.id, m.method, m.params);
|
|
411
|
+
}
|
|
412
|
+
if (typeof m.method === "string") {
|
|
413
|
+
return this.handleNotification(m.method, m.params);
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
376
416
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
417
|
+
/** Abort all active turns and drop session state. */
|
|
418
|
+
close() {
|
|
419
|
+
for (const [, session] of this.sessions) {
|
|
420
|
+
session.abort.abort();
|
|
421
|
+
}
|
|
422
|
+
this.sessions.clear();
|
|
423
|
+
}
|
|
424
|
+
// ────────────────────────────────────────────────────────────────────
|
|
425
|
+
// Requests
|
|
426
|
+
// ────────────────────────────────────────────────────────────────────
|
|
427
|
+
async handleRequest(id, method, params) {
|
|
428
|
+
if (method !== "initialize" && !this.initialized) {
|
|
429
|
+
await this.sendError(id, -32e3, "Not initialized");
|
|
380
430
|
return false;
|
|
381
431
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
432
|
+
try {
|
|
433
|
+
switch (method) {
|
|
434
|
+
case "initialize":
|
|
435
|
+
return await this.handleInitialize(id, params);
|
|
436
|
+
case "authenticate":
|
|
437
|
+
return await this.handleAuthenticate(id, params);
|
|
438
|
+
case "session/new":
|
|
439
|
+
return await this.handleSessionNew(id, params);
|
|
440
|
+
case "session/load":
|
|
441
|
+
return await this.handleSessionLoad(id, params);
|
|
442
|
+
case "session/prompt":
|
|
443
|
+
return await this.handleSessionPrompt(id, params);
|
|
444
|
+
case "session/set_mode":
|
|
445
|
+
return await this.handleSetMode(id, params);
|
|
446
|
+
case "session/set_config_option":
|
|
447
|
+
return await this.handleSetConfigOption(id, params);
|
|
448
|
+
case "session/list":
|
|
449
|
+
return await this.handleSessionList(id);
|
|
450
|
+
default:
|
|
451
|
+
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
} catch (err) {
|
|
455
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
456
|
+
await this.sendError(id, code, message, data);
|
|
457
|
+
return false;
|
|
403
458
|
}
|
|
404
459
|
}
|
|
405
|
-
async
|
|
406
|
-
|
|
407
|
-
|
|
460
|
+
async handleInitialize(id, params) {
|
|
461
|
+
const p = params ?? {};
|
|
462
|
+
const requested = typeof p.protocolVersion === "number" ? p.protocolVersion : 1;
|
|
463
|
+
if (requested !== ACP_PROTOCOL_VERSION) {
|
|
464
|
+
await this.sendError(
|
|
465
|
+
id,
|
|
466
|
+
-32e3,
|
|
467
|
+
`server speaks protocolVersion=${ACP_PROTOCOL_VERSION}, client requested ${requested}`
|
|
468
|
+
);
|
|
469
|
+
return false;
|
|
408
470
|
}
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
async handleInitialize(req, id) {
|
|
412
471
|
this.initialized = true;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
472
|
+
await this.transport.send(toWire({
|
|
473
|
+
jsonrpc: "2.0",
|
|
474
|
+
id,
|
|
475
|
+
result: {
|
|
476
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
477
|
+
agentCapabilities: {
|
|
478
|
+
loadSession: true,
|
|
479
|
+
promptCapabilities: {
|
|
480
|
+
image: false,
|
|
481
|
+
audio: false,
|
|
482
|
+
embeddedContext: true
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
agentInfo: {
|
|
486
|
+
name: this.agentName,
|
|
487
|
+
title: "WrongStack",
|
|
488
|
+
version: WRONGSTACK_VERSION
|
|
489
|
+
},
|
|
490
|
+
// Static options advertised at handshake. They are also
|
|
491
|
+
// re-sent on every `current_mode_update` / `config_option_update`
|
|
492
|
+
// notification so late-joining clients see them.
|
|
493
|
+
authMethods: [],
|
|
494
|
+
modes: this.modes,
|
|
495
|
+
configOptions: this.configOptions
|
|
496
|
+
}
|
|
497
|
+
}));
|
|
421
498
|
return false;
|
|
422
499
|
}
|
|
423
|
-
async
|
|
424
|
-
await this.transport.send({
|
|
500
|
+
async handleAuthenticate(id, _params) {
|
|
501
|
+
await this.transport.send(toWire({
|
|
502
|
+
jsonrpc: "2.0",
|
|
425
503
|
id,
|
|
426
|
-
|
|
427
|
-
|
|
504
|
+
result: { outcome: "unauthenticated" }
|
|
505
|
+
}));
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
async handleSessionNew(id, params) {
|
|
509
|
+
const p = params ?? {};
|
|
510
|
+
const cwd = typeof p.cwd === "string" ? p.cwd : this.defaultCwd;
|
|
511
|
+
const sessionId = `sess_${this.allocId()}`;
|
|
512
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
513
|
+
const state = {
|
|
514
|
+
id: sessionId,
|
|
515
|
+
cwd,
|
|
516
|
+
abort: new AbortController(),
|
|
517
|
+
modeId: DEFAULT_MODE_ID,
|
|
518
|
+
createdAt: now,
|
|
519
|
+
updatedAt: now
|
|
520
|
+
};
|
|
521
|
+
this.sessions.set(sessionId, state);
|
|
522
|
+
this.onSessionNew(state);
|
|
523
|
+
await this.sendNotification({
|
|
524
|
+
sessionId,
|
|
525
|
+
update: {
|
|
526
|
+
sessionUpdate: "current_mode_update",
|
|
527
|
+
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
528
|
+
}
|
|
428
529
|
});
|
|
530
|
+
if (this.configOptions.length > 0) {
|
|
531
|
+
await this.sendNotification({
|
|
532
|
+
sessionId,
|
|
533
|
+
update: {
|
|
534
|
+
sessionUpdate: "config_option_update",
|
|
535
|
+
configOptions: [...this.configOptions]
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
await this.transport.send(toWire({
|
|
540
|
+
jsonrpc: "2.0",
|
|
541
|
+
id,
|
|
542
|
+
result: {
|
|
543
|
+
sessionId,
|
|
544
|
+
modes: this.modes,
|
|
545
|
+
configOptions: this.configOptions
|
|
546
|
+
}
|
|
547
|
+
}));
|
|
429
548
|
return false;
|
|
430
549
|
}
|
|
431
|
-
async
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
550
|
+
async handleSessionLoad(id, params) {
|
|
551
|
+
return this.handleSessionNew(id, params);
|
|
552
|
+
}
|
|
553
|
+
async handleSessionPrompt(id, params) {
|
|
554
|
+
const p = params ?? {};
|
|
555
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
556
|
+
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
557
|
+
await this.sendError(id, -32e3, "unknown or missing sessionId");
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
if (!Array.isArray(p.prompt)) {
|
|
561
|
+
await this.sendError(id, -32602, "prompt must be an array of content blocks");
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
const session = this.sessions.get(sessionId);
|
|
565
|
+
if (session.abort.signal.aborted) {
|
|
566
|
+
session.abort = new AbortController();
|
|
567
|
+
}
|
|
568
|
+
const turnSignal = new AbortController();
|
|
569
|
+
const onCancel = () => turnSignal.abort();
|
|
570
|
+
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
571
|
+
let result;
|
|
449
572
|
try {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
573
|
+
result = await this.runTurn(
|
|
574
|
+
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
575
|
+
(update) => this.sendNotification({ sessionId, update })
|
|
576
|
+
);
|
|
454
577
|
} catch (err) {
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
await this.
|
|
458
|
-
|
|
459
|
-
method: "tools/call",
|
|
460
|
-
result: { content: [{ type: "text", text: msg }], isError: true }
|
|
461
|
-
});
|
|
578
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
579
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
580
|
+
await this.sendError(id, code, message, data);
|
|
581
|
+
return false;
|
|
462
582
|
}
|
|
583
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
584
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
585
|
+
await this.transport.send(toWire({
|
|
586
|
+
jsonrpc: "2.0",
|
|
587
|
+
id,
|
|
588
|
+
result: { stopReason: result.stopReason }
|
|
589
|
+
}));
|
|
463
590
|
return false;
|
|
464
591
|
}
|
|
465
|
-
async
|
|
466
|
-
|
|
467
|
-
|
|
592
|
+
async handleSetMode(id, params) {
|
|
593
|
+
const p = params ?? {};
|
|
594
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
595
|
+
const modeId = typeof p.modeId === "string" ? p.modeId : null;
|
|
596
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
597
|
+
if (!session || !modeId || !this.modes.some((m) => m.id === modeId)) {
|
|
598
|
+
await this.sendError(id, -32602, "invalid sessionId or modeId");
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
session.modeId = modeId;
|
|
602
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
603
|
+
await this.sendNotification({
|
|
604
|
+
sessionId,
|
|
605
|
+
update: { sessionUpdate: "current_mode_update", modeId }
|
|
606
|
+
});
|
|
607
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
|
|
468
608
|
return false;
|
|
469
609
|
}
|
|
470
|
-
|
|
610
|
+
async handleSetConfigOption(id, params) {
|
|
611
|
+
const p = params ?? {};
|
|
612
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
613
|
+
const optionId = typeof p.configOptionId === "string" ? p.configOptionId : null;
|
|
614
|
+
const value = typeof p.value === "string" ? p.value : null;
|
|
615
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
616
|
+
const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
|
|
617
|
+
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
618
|
+
await this.sendError(id, -32602, "invalid sessionId, configOptionId, or value");
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
option.currentValue = value;
|
|
622
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
623
|
+
await this.sendNotification({
|
|
624
|
+
sessionId,
|
|
625
|
+
update: {
|
|
626
|
+
sessionUpdate: "config_option_update",
|
|
627
|
+
configOptions: [...this.configOptions]
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
|
|
631
|
+
return false;
|
|
471
632
|
}
|
|
472
633
|
async handleSessionList(id) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
634
|
+
const sessions = Array.from(this.sessions.values()).map((s) => {
|
|
635
|
+
const out = {
|
|
636
|
+
sessionId: s.id,
|
|
637
|
+
cwd: s.cwd,
|
|
638
|
+
updatedAt: s.updatedAt
|
|
639
|
+
};
|
|
640
|
+
if (s.title !== void 0) out.title = s.title;
|
|
641
|
+
return out;
|
|
477
642
|
});
|
|
643
|
+
await this.transport.send(toWire({
|
|
644
|
+
jsonrpc: "2.0",
|
|
645
|
+
id,
|
|
646
|
+
result: { sessions }
|
|
647
|
+
}));
|
|
478
648
|
return false;
|
|
479
649
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
650
|
+
// ────────────────────────────────────────────────────────────────────
|
|
651
|
+
// Notifications
|
|
652
|
+
// ────────────────────────────────────────────────────────────────────
|
|
653
|
+
async handleNotification(method, params) {
|
|
654
|
+
switch (method) {
|
|
655
|
+
case "session/cancel": {
|
|
656
|
+
const p = params ?? {};
|
|
657
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
658
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
659
|
+
if (session) {
|
|
660
|
+
session.abort.abort();
|
|
661
|
+
}
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
case "exit":
|
|
665
|
+
this.close();
|
|
666
|
+
return true;
|
|
667
|
+
default:
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// ────────────────────────────────────────────────────────────────────
|
|
672
|
+
// Wire helpers
|
|
673
|
+
// ────────────────────────────────────────────────────────────────────
|
|
674
|
+
async sendNotification(params) {
|
|
675
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
|
|
676
|
+
}
|
|
677
|
+
async sendError(id, code, message, data) {
|
|
678
|
+
const error = { code, message };
|
|
679
|
+
if (data !== void 0) error.data = data;
|
|
680
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
681
|
+
}
|
|
682
|
+
allocId() {
|
|
683
|
+
return this.nextId++;
|
|
483
684
|
}
|
|
484
685
|
};
|
|
686
|
+
function errorToJsonRpc(err) {
|
|
687
|
+
if (err && typeof err === "object") {
|
|
688
|
+
const e = err;
|
|
689
|
+
if (typeof e.code === "number" && typeof e.message === "string") {
|
|
690
|
+
const result = {
|
|
691
|
+
code: e.code,
|
|
692
|
+
message: e.message
|
|
693
|
+
};
|
|
694
|
+
if (e.data !== void 0) result.data = e.data;
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
699
|
+
return { code: -32603, message };
|
|
700
|
+
}
|
|
485
701
|
var WrongStackACPServer = class {
|
|
486
702
|
transport;
|
|
487
|
-
registry;
|
|
488
703
|
handler;
|
|
489
704
|
running = false;
|
|
490
|
-
constructor(opts) {
|
|
705
|
+
constructor(opts = {}) {
|
|
491
706
|
this.transport = new StdioTransport();
|
|
492
|
-
|
|
493
|
-
this.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// project metadata so ACP tools can self-contextualise.
|
|
500
|
-
// Tracked in docs/notes/refactor-2026-06-05.md §5.1.
|
|
501
|
-
{}
|
|
502
|
-
);
|
|
707
|
+
const runTurn = opts.runTurn ?? defaultEchoRunTurn;
|
|
708
|
+
this.handler = new ACPProtocolHandler({
|
|
709
|
+
transport: this.transport,
|
|
710
|
+
defaultCwd: opts.defaultCwd ?? process.cwd(),
|
|
711
|
+
runTurn,
|
|
712
|
+
agentName: opts.agentName
|
|
713
|
+
});
|
|
503
714
|
}
|
|
504
715
|
/**
|
|
505
716
|
* Start the server. Blocks until the client disconnects.
|
|
506
717
|
*
|
|
507
|
-
* 1.
|
|
508
|
-
*
|
|
509
|
-
* 2. Loop: read messages, dispatch to handler, until EOF
|
|
510
|
-
*
|
|
511
|
-
* Single dispatch path: every inbound message is read exactly once
|
|
512
|
-
* from the transport and passed to the protocol handler exactly once.
|
|
513
|
-
* An earlier version combined a `transport.onMessage` callback with
|
|
514
|
-
* this read loop, which caused every message to be processed twice
|
|
515
|
-
* (once by the callback, once by the loop) — duplicate tool calls
|
|
516
|
-
* and duplicate responses to the client. See the ACP double-dispatch
|
|
517
|
-
* fix in the security audit (P1-001).
|
|
718
|
+
* 1. Print the legacy `[wstack-acp]\n` marker so the client knows the
|
|
719
|
+
* process is the ACP server (the old `StdioTransport` handshake).
|
|
720
|
+
* 2. Loop: read messages, dispatch to the handler, until EOF / error.
|
|
518
721
|
*/
|
|
519
722
|
async start() {
|
|
520
723
|
this.transport.sendStartupMarker();
|
|
@@ -533,8 +736,11 @@ var WrongStackACPServer = class {
|
|
|
533
736
|
this.transport.close();
|
|
534
737
|
}
|
|
535
738
|
};
|
|
739
|
+
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
740
|
+
return { stopReason: "end_turn" };
|
|
741
|
+
};
|
|
536
742
|
async function main() {
|
|
537
|
-
const server = new WrongStackACPServer(
|
|
743
|
+
const server = new WrongStackACPServer();
|
|
538
744
|
await server.start();
|
|
539
745
|
}
|
|
540
746
|
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
@@ -550,32 +756,6 @@ var DEFAULT_OPTIONS = {
|
|
|
550
756
|
pollIntervalMs: 500,
|
|
551
757
|
totalTimeoutMs: 12e4
|
|
552
758
|
};
|
|
553
|
-
function extractTextFromContent(blocks) {
|
|
554
|
-
const parts = [];
|
|
555
|
-
for (const b of blocks) {
|
|
556
|
-
if (b.type === "text") parts.push(b.text);
|
|
557
|
-
else if (b.type === "resource") parts.push(`[resource: ${b.resource.uri}]`);
|
|
558
|
-
else if (b.type === "image") parts.push(`[image: ${b.data.slice(0, 20)}...]`);
|
|
559
|
-
else if (b.type === "progress") {
|
|
560
|
-
if (b.messages?.length) parts.push(b.messages.join("\n"));
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
return parts.join("\n");
|
|
564
|
-
}
|
|
565
|
-
function parseToolResponse(taskId, subagentId, response) {
|
|
566
|
-
const blocks = response.result.content;
|
|
567
|
-
const text = extractTextFromContent(blocks);
|
|
568
|
-
const isError = response.result.isError || text.toLowerCase().includes("error") || text.toLowerCase().includes("failed");
|
|
569
|
-
return {
|
|
570
|
-
taskId,
|
|
571
|
-
subagentId,
|
|
572
|
-
status: isError ? "failed" : "success",
|
|
573
|
-
result: text,
|
|
574
|
-
iterations: 1,
|
|
575
|
-
toolCalls: 1,
|
|
576
|
-
durationMs: 0
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
759
|
var ToolTranslator = class {
|
|
580
760
|
opts;
|
|
581
761
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -617,12 +797,12 @@ var ToolTranslator = class {
|
|
|
617
797
|
id: callId,
|
|
618
798
|
params: { name, arguments: args }
|
|
619
799
|
});
|
|
620
|
-
return new Promise((
|
|
800
|
+
return new Promise((resolve3, reject) => {
|
|
621
801
|
const timeout = setTimeout(() => {
|
|
622
802
|
this.pending.delete(callId);
|
|
623
803
|
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
624
804
|
}, this.opts.totalTimeoutMs);
|
|
625
|
-
this.pending.set(callId, { resolve, reject, timeout });
|
|
805
|
+
this.pending.set(callId, { resolve: resolve3, reject, timeout });
|
|
626
806
|
});
|
|
627
807
|
}
|
|
628
808
|
cancelAll() {
|
|
@@ -632,6 +812,771 @@ var ToolTranslator = class {
|
|
|
632
812
|
this.pending.clear();
|
|
633
813
|
}
|
|
634
814
|
};
|
|
815
|
+
var FsError = class extends Error {
|
|
816
|
+
code;
|
|
817
|
+
path;
|
|
818
|
+
constructor(code, path3, message) {
|
|
819
|
+
super(message);
|
|
820
|
+
this.name = "FsError";
|
|
821
|
+
this.code = code;
|
|
822
|
+
this.path = path3;
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
var FileServer = class {
|
|
826
|
+
root;
|
|
827
|
+
timeoutMs;
|
|
828
|
+
constructor(opts) {
|
|
829
|
+
this.root = path.resolve(opts.projectRoot);
|
|
830
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
831
|
+
}
|
|
832
|
+
/** Read a text file. Returns the content as a string. */
|
|
833
|
+
async readTextFile(params) {
|
|
834
|
+
const safe = this.resolveInside(params.path);
|
|
835
|
+
const controller = new AbortController();
|
|
836
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
837
|
+
try {
|
|
838
|
+
const content = await fsp.readFile(safe, {
|
|
839
|
+
encoding: "utf8",
|
|
840
|
+
signal: controller.signal
|
|
841
|
+
});
|
|
842
|
+
return { content };
|
|
843
|
+
} catch (err) {
|
|
844
|
+
if (controller.signal.aborted) {
|
|
845
|
+
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
846
|
+
}
|
|
847
|
+
throw mapFsError(err, safe);
|
|
848
|
+
} finally {
|
|
849
|
+
clearTimeout(timer);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/** Write a text file. Atomic via write-then-rename. */
|
|
853
|
+
async writeTextFile(params) {
|
|
854
|
+
const safe = this.resolveInside(params.path);
|
|
855
|
+
const controller = new AbortController();
|
|
856
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
857
|
+
const tmp = `${safe}.${randomHex(4)}.tmp`;
|
|
858
|
+
try {
|
|
859
|
+
await fsp.writeFile(tmp, params.content, {
|
|
860
|
+
encoding: "utf8",
|
|
861
|
+
signal: controller.signal
|
|
862
|
+
});
|
|
863
|
+
await fsp.rename(tmp, safe);
|
|
864
|
+
} catch (err) {
|
|
865
|
+
try {
|
|
866
|
+
await fsp.unlink(tmp);
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
if (controller.signal.aborted) {
|
|
870
|
+
throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
|
|
871
|
+
}
|
|
872
|
+
throw mapFsError(err, safe);
|
|
873
|
+
} finally {
|
|
874
|
+
clearTimeout(timer);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Resolve a path; throw `FsError('OUTSIDE_ROOT')` if the result is
|
|
879
|
+
* not under the project root. Symlinks are not followed here — we
|
|
880
|
+
* operate on the textual path. A future hardening pass can
|
|
881
|
+
* `fs.realpath` each access to catch symlink escapes.
|
|
882
|
+
*/
|
|
883
|
+
resolveInside(p) {
|
|
884
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
885
|
+
throw new FsError("INVALID_PATH", p, "path is empty or not a string");
|
|
886
|
+
}
|
|
887
|
+
if (!path.isAbsolute(p)) {
|
|
888
|
+
throw new FsError("INVALID_PATH", p, "path must be absolute (ACP requirement)");
|
|
889
|
+
}
|
|
890
|
+
const resolved = path.resolve(p);
|
|
891
|
+
const rootWithSep = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;
|
|
892
|
+
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
893
|
+
throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
|
|
894
|
+
}
|
|
895
|
+
return resolved;
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
function mapFsError(err, p) {
|
|
899
|
+
const code = err?.code;
|
|
900
|
+
if (code === "ENOENT") return new FsError("ENOENT", p, `no such file: ${p}`);
|
|
901
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
902
|
+
return new FsError("EACCES", p, `permission denied: ${p}`);
|
|
903
|
+
}
|
|
904
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
905
|
+
return new FsError("INVALID_PATH", p, msg);
|
|
906
|
+
}
|
|
907
|
+
function randomHex(bytes) {
|
|
908
|
+
let out = "";
|
|
909
|
+
for (let i = 0; i < bytes * 2; i++) {
|
|
910
|
+
out += Math.floor(Math.random() * 16).toString(16);
|
|
911
|
+
}
|
|
912
|
+
return out;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/client/permission.ts
|
|
916
|
+
var defaultPermissionPolicy = async (req) => {
|
|
917
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
918
|
+
const ranked = [...req.options].sort((a, b) => {
|
|
919
|
+
const score = (k) => {
|
|
920
|
+
if (k === "allow_always") return 0;
|
|
921
|
+
if (k === "allow_once") return 1;
|
|
922
|
+
if (k === "reject_once") return 2;
|
|
923
|
+
return 3;
|
|
924
|
+
};
|
|
925
|
+
return score(a.kind) - score(b.kind);
|
|
926
|
+
});
|
|
927
|
+
const chosen = ranked[0];
|
|
928
|
+
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
929
|
+
return { outcome: "cancelled" };
|
|
930
|
+
}
|
|
931
|
+
return { outcome: "selected", optionId: chosen.optionId };
|
|
932
|
+
};
|
|
933
|
+
var TerminalServer = class {
|
|
934
|
+
terminals = /* @__PURE__ */ new Map();
|
|
935
|
+
projectRoot;
|
|
936
|
+
commandTimeoutMs;
|
|
937
|
+
outputByteLimit;
|
|
938
|
+
nextId = 1;
|
|
939
|
+
constructor(opts) {
|
|
940
|
+
this.projectRoot = path.resolve(opts.projectRoot);
|
|
941
|
+
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
942
|
+
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
943
|
+
if (opts.signal) {
|
|
944
|
+
opts.signal.addEventListener("abort", () => this.releaseAll());
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
948
|
+
create(params) {
|
|
949
|
+
const id = `term_${this.nextId++}`;
|
|
950
|
+
const cwd = this.resolveCwd(params.cwd);
|
|
951
|
+
const proc = spawn(params.command, params.args ?? [], {
|
|
952
|
+
cwd,
|
|
953
|
+
env: this.buildEnv(params.env),
|
|
954
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
955
|
+
windowsHide: true
|
|
956
|
+
// shell: false on purpose. The terminal server is invoked with
|
|
957
|
+
// the agent's explicit argv; turning on shell-mode would make
|
|
958
|
+
// the command a single shell-parsed string, which breaks
|
|
959
|
+
// Windows cmd quoting for the common case of running node with
|
|
960
|
+
// `-e "<script>"`. If a future feature needs shell features
|
|
961
|
+
// (pipes, redirects), it should be opt-in per-call, not the
|
|
962
|
+
// default.
|
|
963
|
+
});
|
|
964
|
+
const state = {
|
|
965
|
+
proc,
|
|
966
|
+
cwd,
|
|
967
|
+
command: params.command,
|
|
968
|
+
args: params.args ?? [],
|
|
969
|
+
output: "",
|
|
970
|
+
retainedBytes: 0,
|
|
971
|
+
truncated: false,
|
|
972
|
+
exitStatus: void 0,
|
|
973
|
+
timeoutHandle: null,
|
|
974
|
+
exitPromise: new Promise((resolve3) => {
|
|
975
|
+
proc.on("close", (code, signalName) => {
|
|
976
|
+
if (state.timeoutHandle) {
|
|
977
|
+
clearTimeout(state.timeoutHandle);
|
|
978
|
+
state.timeoutHandle = null;
|
|
979
|
+
}
|
|
980
|
+
const exitStatus = {
|
|
981
|
+
exitCode: typeof code === "number" ? code : null,
|
|
982
|
+
signal: typeof signalName === "string" ? signalName : null
|
|
983
|
+
};
|
|
984
|
+
state.exitStatus = exitStatus;
|
|
985
|
+
resolve3(exitStatus);
|
|
986
|
+
});
|
|
987
|
+
proc.on("error", (err) => {
|
|
988
|
+
if (state.timeoutHandle) {
|
|
989
|
+
clearTimeout(state.timeoutHandle);
|
|
990
|
+
state.timeoutHandle = null;
|
|
991
|
+
}
|
|
992
|
+
const exitStatus = { exitCode: 127, signal: null };
|
|
993
|
+
state.exitStatus = exitStatus;
|
|
994
|
+
state.output += `[spawn error] ${err.message}
|
|
995
|
+
`;
|
|
996
|
+
state.retainedBytes += Buffer.byteLength(state.output, "utf8");
|
|
997
|
+
resolve3(exitStatus);
|
|
998
|
+
});
|
|
999
|
+
})
|
|
1000
|
+
};
|
|
1001
|
+
const perCallByteLimit = params.outputByteLimit ?? this.outputByteLimit;
|
|
1002
|
+
proc.stdout?.setEncoding("utf8");
|
|
1003
|
+
proc.stderr?.setEncoding("utf8");
|
|
1004
|
+
const onData = (chunk) => {
|
|
1005
|
+
state.output += chunk;
|
|
1006
|
+
state.retainedBytes = Buffer.byteLength(state.output, "utf8");
|
|
1007
|
+
while (state.retainedBytes > perCallByteLimit) {
|
|
1008
|
+
const trimmed = state.output.slice(1);
|
|
1009
|
+
state.output = trimmed;
|
|
1010
|
+
const newBytes = Buffer.byteLength(state.output, "utf8");
|
|
1011
|
+
if (newBytes >= state.retainedBytes) {
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
state.retainedBytes = newBytes;
|
|
1015
|
+
state.truncated = true;
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
proc.stdout?.on("data", onData);
|
|
1019
|
+
proc.stderr?.on("data", onData);
|
|
1020
|
+
state.timeoutHandle = setTimeout(() => {
|
|
1021
|
+
try {
|
|
1022
|
+
proc.kill("SIGTERM");
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
}, this.commandTimeoutMs);
|
|
1026
|
+
this.terminals.set(id, state);
|
|
1027
|
+
return { terminalId: id };
|
|
1028
|
+
}
|
|
1029
|
+
/** Return captured output and (if available) the exit status. */
|
|
1030
|
+
output(terminalId) {
|
|
1031
|
+
const state = this.terminals.get(terminalId);
|
|
1032
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1033
|
+
return {
|
|
1034
|
+
output: state.output,
|
|
1035
|
+
truncated: state.truncated,
|
|
1036
|
+
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
/** Block until the process exits. Resolves with the exit status. */
|
|
1040
|
+
async waitForExit(terminalId) {
|
|
1041
|
+
const state = this.terminals.get(terminalId);
|
|
1042
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1043
|
+
return state.exitPromise;
|
|
1044
|
+
}
|
|
1045
|
+
/** Kill the process but keep the terminal record (agent can still read output). */
|
|
1046
|
+
kill(terminalId) {
|
|
1047
|
+
const state = this.terminals.get(terminalId);
|
|
1048
|
+
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1049
|
+
try {
|
|
1050
|
+
state.proc.kill("SIGTERM");
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
/** Kill the process if alive and remove the record. */
|
|
1055
|
+
release(terminalId) {
|
|
1056
|
+
const state = this.terminals.get(terminalId);
|
|
1057
|
+
if (!state) return;
|
|
1058
|
+
if (state.timeoutHandle) {
|
|
1059
|
+
clearTimeout(state.timeoutHandle);
|
|
1060
|
+
state.timeoutHandle = null;
|
|
1061
|
+
}
|
|
1062
|
+
try {
|
|
1063
|
+
state.proc.kill("SIGKILL");
|
|
1064
|
+
} catch {
|
|
1065
|
+
}
|
|
1066
|
+
this.terminals.delete(terminalId);
|
|
1067
|
+
}
|
|
1068
|
+
/** Kill all active terminals. Used on session close. */
|
|
1069
|
+
releaseAll() {
|
|
1070
|
+
for (const id of [...this.terminals.keys()]) {
|
|
1071
|
+
this.release(id);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
resolveCwd(cwd) {
|
|
1075
|
+
if (!cwd) return this.projectRoot;
|
|
1076
|
+
const resolved = path.resolve(cwd);
|
|
1077
|
+
const rootWithSep = this.projectRoot.endsWith(path.sep) ? this.projectRoot : this.projectRoot + path.sep;
|
|
1078
|
+
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
1079
|
+
return this.projectRoot;
|
|
1080
|
+
}
|
|
1081
|
+
return resolved;
|
|
1082
|
+
}
|
|
1083
|
+
buildEnv(agentEnv) {
|
|
1084
|
+
const env = { ...process.env };
|
|
1085
|
+
if (process.platform === "win32") {
|
|
1086
|
+
if (env.Path !== void 0 && env.PATH === void 0) env.PATH = env.Path;
|
|
1087
|
+
if (env.PATHEXT !== void 0 && env.PATHEXT_CASE === void 0) {
|
|
1088
|
+
env.PATHEXT_CASE = env.PATHEXT;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (agentEnv) {
|
|
1092
|
+
for (const { name, value } of agentEnv) {
|
|
1093
|
+
env[name] = value;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return env;
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// src/client/acp-session.ts
|
|
1101
|
+
var ACPSessionError = class extends Error {
|
|
1102
|
+
kind;
|
|
1103
|
+
cause;
|
|
1104
|
+
constructor(kind, message, cause) {
|
|
1105
|
+
super(message);
|
|
1106
|
+
this.name = "ACPSessionError";
|
|
1107
|
+
this.kind = kind;
|
|
1108
|
+
this.cause = cause;
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
function isJsonRpcError(v) {
|
|
1112
|
+
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
1113
|
+
}
|
|
1114
|
+
var ACPSession = class _ACPSession {
|
|
1115
|
+
transport;
|
|
1116
|
+
fileServer;
|
|
1117
|
+
terminalServer;
|
|
1118
|
+
permissionPolicy;
|
|
1119
|
+
timeoutMs;
|
|
1120
|
+
opts;
|
|
1121
|
+
state = "init";
|
|
1122
|
+
sessionId = null;
|
|
1123
|
+
/** Pending outbound requests (initialize, session/new, session/prompt, etc). */
|
|
1124
|
+
pending = /* @__PURE__ */ new Map();
|
|
1125
|
+
nextId = 1;
|
|
1126
|
+
/** True after close() has been called. */
|
|
1127
|
+
closed = false;
|
|
1128
|
+
constructor(opts, transport) {
|
|
1129
|
+
this.opts = opts;
|
|
1130
|
+
this.transport = transport;
|
|
1131
|
+
this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
1132
|
+
const fsOpts = {
|
|
1133
|
+
projectRoot: opts.projectRoot
|
|
1134
|
+
};
|
|
1135
|
+
if (opts.fsTimeoutMs !== void 0) fsOpts.timeoutMs = opts.fsTimeoutMs;
|
|
1136
|
+
this.fileServer = new FileServer(fsOpts);
|
|
1137
|
+
const termOpts = {
|
|
1138
|
+
projectRoot: opts.projectRoot
|
|
1139
|
+
};
|
|
1140
|
+
if (opts.terminalTimeoutMs !== void 0) {
|
|
1141
|
+
termOpts.commandTimeoutMs = opts.terminalTimeoutMs;
|
|
1142
|
+
}
|
|
1143
|
+
if (opts.terminalOutputByteLimit !== void 0) {
|
|
1144
|
+
termOpts.outputByteLimit = opts.terminalOutputByteLimit;
|
|
1145
|
+
}
|
|
1146
|
+
this.terminalServer = new TerminalServer(termOpts);
|
|
1147
|
+
this.permissionPolicy = opts.permissionPolicy ?? defaultPermissionPolicy;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Spawn the child, run the initialize handshake, install the
|
|
1151
|
+
* message dispatch, and return a ready session.
|
|
1152
|
+
*/
|
|
1153
|
+
static async start(opts) {
|
|
1154
|
+
const transportOpts = {
|
|
1155
|
+
command: opts.command,
|
|
1156
|
+
args: opts.args ? [...opts.args] : [],
|
|
1157
|
+
handshakeTimeoutMs: 3e4,
|
|
1158
|
+
// ACPSession is the v1 CLIENT side: it speaks to external agents
|
|
1159
|
+
// (Claude Code, Gemini CLI, …) that do NOT emit a `[wstack-acp]\n`
|
|
1160
|
+
// startup marker. The transport should treat the child as ready
|
|
1161
|
+
// as soon as the process is spawned and stdout is flowing.
|
|
1162
|
+
skipHandshakeMarker: true
|
|
1163
|
+
};
|
|
1164
|
+
if (opts.env !== void 0) transportOpts.env = opts.env;
|
|
1165
|
+
if (opts.cwd !== void 0) transportOpts.cwd = opts.cwd;
|
|
1166
|
+
const transport = new ClientTransport(transportOpts);
|
|
1167
|
+
try {
|
|
1168
|
+
await transport.start();
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1171
|
+
throw new ACPSessionError("spawn_failed", `failed to spawn ${opts.command}: ${msg}`, err);
|
|
1172
|
+
}
|
|
1173
|
+
const session = new _ACPSession(opts, transport);
|
|
1174
|
+
transport.onMessage((msg) => session.handleMessage(msg));
|
|
1175
|
+
try {
|
|
1176
|
+
await session.initialize();
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
try {
|
|
1179
|
+
transport.stop();
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
throw err;
|
|
1183
|
+
}
|
|
1184
|
+
return session;
|
|
1185
|
+
}
|
|
1186
|
+
async initialize() {
|
|
1187
|
+
const id = this.allocId();
|
|
1188
|
+
const result = await this.sendRequest(id, "initialize", {
|
|
1189
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
1190
|
+
clientCapabilities: {
|
|
1191
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
1192
|
+
terminal: true,
|
|
1193
|
+
promptCapabilities: { image: false, audio: false, embeddedContext: true }
|
|
1194
|
+
},
|
|
1195
|
+
clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.263.0" }
|
|
1196
|
+
});
|
|
1197
|
+
if (isJsonRpcError(result)) {
|
|
1198
|
+
throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
|
|
1199
|
+
}
|
|
1200
|
+
if (typeof result !== "object" || result === null || typeof result.protocolVersion !== "number") {
|
|
1201
|
+
throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
|
|
1202
|
+
}
|
|
1203
|
+
const r = result;
|
|
1204
|
+
if (r.protocolVersion !== ACP_PROTOCOL_VERSION) {
|
|
1205
|
+
throw new ACPSessionError(
|
|
1206
|
+
"unsupported_capability",
|
|
1207
|
+
`agent speaks protocolVersion=${r.protocolVersion}, client speaks ${ACP_PROTOCOL_VERSION}`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
this.state = "ready";
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Run one prompt turn. Creates a session if needed, sends the
|
|
1214
|
+
* prompt, streams session/update notifications, and resolves with
|
|
1215
|
+
* the agent's response.
|
|
1216
|
+
*
|
|
1217
|
+
* Cancellation: if `signal` aborts mid-prompt, we send
|
|
1218
|
+
* `session/cancel` (a notification per spec) and keep accepting
|
|
1219
|
+
* updates until the agent returns with `stopReason: 'cancelled'`.
|
|
1220
|
+
* The result is the same shape as a normal turn, with
|
|
1221
|
+
* `stopReason === 'cancelled'`.
|
|
1222
|
+
*/
|
|
1223
|
+
async prompt(text, signal) {
|
|
1224
|
+
if (this.closed) {
|
|
1225
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
1226
|
+
}
|
|
1227
|
+
if (this.state !== "ready" && this.state !== "done") {
|
|
1228
|
+
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
1229
|
+
}
|
|
1230
|
+
if (signal.aborted) {
|
|
1231
|
+
return { text: "", stopReason: "cancelled", hasText: false };
|
|
1232
|
+
}
|
|
1233
|
+
if (!this.sessionId) {
|
|
1234
|
+
await this.createSession();
|
|
1235
|
+
}
|
|
1236
|
+
this.resetScratch();
|
|
1237
|
+
const promptId = this.allocId();
|
|
1238
|
+
const turnPromise = this.sendRequest(
|
|
1239
|
+
promptId,
|
|
1240
|
+
"session/prompt",
|
|
1241
|
+
{
|
|
1242
|
+
sessionId: this.sessionId,
|
|
1243
|
+
prompt: [textContent(text)]
|
|
1244
|
+
},
|
|
1245
|
+
this.timeoutMs
|
|
1246
|
+
);
|
|
1247
|
+
let cancelled = false;
|
|
1248
|
+
const onAbort = () => {
|
|
1249
|
+
cancelled = true;
|
|
1250
|
+
this.transport.send({ method: "session/cancel", params: { sessionId: this.sessionId } }).catch(() => {
|
|
1251
|
+
});
|
|
1252
|
+
};
|
|
1253
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1254
|
+
this.state = "prompting";
|
|
1255
|
+
let response;
|
|
1256
|
+
try {
|
|
1257
|
+
response = await turnPromise;
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
this.state = "done";
|
|
1260
|
+
signal.removeEventListener("abort", onAbort);
|
|
1261
|
+
if (cancelled || signal.aborted) {
|
|
1262
|
+
throw new ACPSessionError("aborted", "prompt was aborted by the parent");
|
|
1263
|
+
}
|
|
1264
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1265
|
+
throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
|
|
1266
|
+
} finally {
|
|
1267
|
+
signal.removeEventListener("abort", onAbort);
|
|
1268
|
+
}
|
|
1269
|
+
this.state = "done";
|
|
1270
|
+
if (isJsonRpcError(response)) {
|
|
1271
|
+
throw new ACPSessionError("prompt_failed", `agent error: ${response.message}`, response);
|
|
1272
|
+
}
|
|
1273
|
+
const stopReason = response.stopReason ?? "end_turn";
|
|
1274
|
+
const finalText = this.scratch.text;
|
|
1275
|
+
return {
|
|
1276
|
+
text: finalText,
|
|
1277
|
+
stopReason,
|
|
1278
|
+
hasText: finalText.length > 0,
|
|
1279
|
+
usage: this.scratch.usage,
|
|
1280
|
+
plan: this.scratch.plan
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
async createSession() {
|
|
1284
|
+
const id = this.allocId();
|
|
1285
|
+
const result = await this.sendRequest(id, "session/new", {
|
|
1286
|
+
cwd: this.opts.cwd ?? this.opts.projectRoot,
|
|
1287
|
+
mcpServers: []
|
|
1288
|
+
});
|
|
1289
|
+
if (isJsonRpcError(result)) {
|
|
1290
|
+
throw new ACPSessionError(
|
|
1291
|
+
"session_create_failed",
|
|
1292
|
+
`session/new failed: ${result.message}`,
|
|
1293
|
+
result
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
const sessionId = result.sessionId;
|
|
1297
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
1298
|
+
throw new ACPSessionError(
|
|
1299
|
+
"protocol_error",
|
|
1300
|
+
"session/new returned no sessionId",
|
|
1301
|
+
result
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
this.sessionId = sessionId;
|
|
1305
|
+
}
|
|
1306
|
+
/** Tear down the session and kill the child process. */
|
|
1307
|
+
async close() {
|
|
1308
|
+
if (this.closed) return;
|
|
1309
|
+
this.closed = true;
|
|
1310
|
+
this.state = "closed";
|
|
1311
|
+
this.terminalServer.releaseAll();
|
|
1312
|
+
for (const [, p] of this.pending) {
|
|
1313
|
+
clearTimeout(p.timeoutHandle);
|
|
1314
|
+
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
1315
|
+
}
|
|
1316
|
+
this.pending.clear();
|
|
1317
|
+
try {
|
|
1318
|
+
this.transport.stop();
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1323
|
+
// Wire layer
|
|
1324
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1325
|
+
allocId() {
|
|
1326
|
+
return this.nextId++;
|
|
1327
|
+
}
|
|
1328
|
+
async sendRequest(id, method, params, timeoutMs) {
|
|
1329
|
+
return new Promise((resolve3, reject) => {
|
|
1330
|
+
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
1331
|
+
const handle = setTimeout(() => {
|
|
1332
|
+
this.pending.delete(id);
|
|
1333
|
+
reject(
|
|
1334
|
+
new ACPSessionError(
|
|
1335
|
+
"protocol_error",
|
|
1336
|
+
`${method} timed out after ${effectiveTimeout}ms`
|
|
1337
|
+
)
|
|
1338
|
+
);
|
|
1339
|
+
}, effectiveTimeout);
|
|
1340
|
+
this.pending.set(id, {
|
|
1341
|
+
method,
|
|
1342
|
+
resolve: resolve3,
|
|
1343
|
+
reject,
|
|
1344
|
+
timeoutMs: effectiveTimeout,
|
|
1345
|
+
timeoutHandle: handle
|
|
1346
|
+
});
|
|
1347
|
+
this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
|
1348
|
+
clearTimeout(handle);
|
|
1349
|
+
this.pending.delete(id);
|
|
1350
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1351
|
+
reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
|
|
1352
|
+
});
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
handleMessage(msg) {
|
|
1356
|
+
if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
|
|
1357
|
+
const pending = this.pending.get(msg.id);
|
|
1358
|
+
if (!pending) return;
|
|
1359
|
+
clearTimeout(pending.timeoutHandle);
|
|
1360
|
+
this.pending.delete(msg.id);
|
|
1361
|
+
if (msg.error !== void 0) {
|
|
1362
|
+
pending.reject(new Error(msg.error.message ?? "unknown JSON-RPC error"));
|
|
1363
|
+
} else {
|
|
1364
|
+
pending.resolve(msg.result);
|
|
1365
|
+
}
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (msg.method === "session/update") {
|
|
1369
|
+
this.handleUpdate(msg);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (msg.method === "session/request_permission") {
|
|
1373
|
+
void this.handlePermissionRequest(msg);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
|
|
1377
|
+
void this.handleFsRequest(msg);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (msg.method && msg.method.startsWith("terminal/")) {
|
|
1381
|
+
void this.handleTerminalRequest(msg);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (msg.method) {
|
|
1385
|
+
console.warn(`[acp-session] unhandled method: ${msg.method}`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
handleUpdate(msg) {
|
|
1389
|
+
const update = msg.params?.update;
|
|
1390
|
+
if (typeof update !== "object" || update === null) return;
|
|
1391
|
+
const u = update;
|
|
1392
|
+
switch (u.sessionUpdate) {
|
|
1393
|
+
case "agent_message_chunk": {
|
|
1394
|
+
const text = extractText(u.content);
|
|
1395
|
+
if (text) this.accumulatedText(text);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
case "thought_chunk":
|
|
1399
|
+
return;
|
|
1400
|
+
case "tool_call":
|
|
1401
|
+
case "tool_call_update":
|
|
1402
|
+
return;
|
|
1403
|
+
case "plan":
|
|
1404
|
+
if (Array.isArray(u.entries)) {
|
|
1405
|
+
this.accumulatedPlan(u.entries);
|
|
1406
|
+
}
|
|
1407
|
+
return;
|
|
1408
|
+
case "usage_update":
|
|
1409
|
+
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
1410
|
+
this.accumulatedUsage({
|
|
1411
|
+
used: u.used,
|
|
1412
|
+
size: u.size,
|
|
1413
|
+
...typeof u.cost === "object" && u.cost !== null ? {
|
|
1414
|
+
cost: u.cost
|
|
1415
|
+
} : {}
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
return;
|
|
1419
|
+
case "available_commands_update":
|
|
1420
|
+
case "current_mode_update":
|
|
1421
|
+
case "config_option_update":
|
|
1422
|
+
case "session_info_update":
|
|
1423
|
+
case "user_message_chunk":
|
|
1424
|
+
return;
|
|
1425
|
+
default:
|
|
1426
|
+
console.warn(`[acp-session] unhandled sessionUpdate: ${u.sessionUpdate}`);
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
// Per-prompt scratch state. Reset at the start of each prompt() and
|
|
1431
|
+
// read at the end to assemble the ACPSessionRunResult. The stream
|
|
1432
|
+
// pump writes to it via the three `accumulated*` helpers below.
|
|
1433
|
+
scratch = { text: "" };
|
|
1434
|
+
accumulatedText(chunk) {
|
|
1435
|
+
this.scratch.text += chunk;
|
|
1436
|
+
}
|
|
1437
|
+
accumulatedPlan(entries) {
|
|
1438
|
+
this.scratch.plan = entries;
|
|
1439
|
+
}
|
|
1440
|
+
accumulatedUsage(u) {
|
|
1441
|
+
this.scratch.usage = u;
|
|
1442
|
+
}
|
|
1443
|
+
resetScratch() {
|
|
1444
|
+
this.scratch = { text: "" };
|
|
1445
|
+
}
|
|
1446
|
+
async handlePermissionRequest(msg) {
|
|
1447
|
+
const id = msg.id;
|
|
1448
|
+
if (id === void 0) return;
|
|
1449
|
+
const params = msg.params;
|
|
1450
|
+
const toolCall = params?.toolCall;
|
|
1451
|
+
const options = Array.isArray(params?.options) ? params.options : [];
|
|
1452
|
+
if (!toolCall) {
|
|
1453
|
+
await this.transport.send({
|
|
1454
|
+
id,
|
|
1455
|
+
method: "session/request_permission",
|
|
1456
|
+
error: { code: -32602, message: "toolCall is required" }
|
|
1457
|
+
});
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const policyAbort = new AbortController();
|
|
1461
|
+
const outcome = await this.permissionPolicy({
|
|
1462
|
+
toolCall,
|
|
1463
|
+
options,
|
|
1464
|
+
signal: policyAbort.signal
|
|
1465
|
+
});
|
|
1466
|
+
await this.transport.send({
|
|
1467
|
+
id,
|
|
1468
|
+
method: "session/request_permission",
|
|
1469
|
+
result: { outcome }
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
async handleFsRequest(msg) {
|
|
1473
|
+
const id = msg.id;
|
|
1474
|
+
if (id === void 0) return;
|
|
1475
|
+
const params = msg.params;
|
|
1476
|
+
if (!params?.path) {
|
|
1477
|
+
await this.transport.send({
|
|
1478
|
+
id,
|
|
1479
|
+
method: msg.method,
|
|
1480
|
+
error: { code: -32602, message: "path is required" }
|
|
1481
|
+
});
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
try {
|
|
1485
|
+
if (msg.method === "fs/read_text_file") {
|
|
1486
|
+
const result = await this.fileServer.readTextFile({
|
|
1487
|
+
sessionId: params.sessionId ?? "",
|
|
1488
|
+
path: params.path
|
|
1489
|
+
});
|
|
1490
|
+
await this.transport.send({ id, method: msg.method, result });
|
|
1491
|
+
} else {
|
|
1492
|
+
await this.fileServer.writeTextFile({
|
|
1493
|
+
sessionId: params.sessionId ?? "",
|
|
1494
|
+
path: params.path,
|
|
1495
|
+
content: params.content ?? ""
|
|
1496
|
+
});
|
|
1497
|
+
await this.transport.send({ id, method: msg.method, result: {} });
|
|
1498
|
+
}
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
const code = err instanceof FsError ? -32602 : -32603;
|
|
1501
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1502
|
+
await this.transport.send({ id, method: msg.method, error: { code, message } });
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
async handleTerminalRequest(msg) {
|
|
1506
|
+
const id = msg.id;
|
|
1507
|
+
if (id === void 0) return;
|
|
1508
|
+
const params = msg.params ?? {};
|
|
1509
|
+
try {
|
|
1510
|
+
switch (msg.method) {
|
|
1511
|
+
case "terminal/create": {
|
|
1512
|
+
const createOpts = {
|
|
1513
|
+
sessionId: String(params.sessionId ?? ""),
|
|
1514
|
+
command: String(params.command ?? ""),
|
|
1515
|
+
args: Array.isArray(params.args) ? params.args : []
|
|
1516
|
+
};
|
|
1517
|
+
if (Array.isArray(params.env)) {
|
|
1518
|
+
createOpts.env = params.env;
|
|
1519
|
+
}
|
|
1520
|
+
if (typeof params.cwd === "string") {
|
|
1521
|
+
createOpts.cwd = params.cwd;
|
|
1522
|
+
}
|
|
1523
|
+
if (typeof params.outputByteLimit === "number") {
|
|
1524
|
+
createOpts.outputByteLimit = params.outputByteLimit;
|
|
1525
|
+
}
|
|
1526
|
+
const result = this.terminalServer.create(createOpts);
|
|
1527
|
+
await this.transport.send({ id, method: msg.method, result });
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
case "terminal/output": {
|
|
1531
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1532
|
+
const out = this.terminalServer.output(terminalId);
|
|
1533
|
+
await this.transport.send({ id, method: msg.method, result: out });
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
case "terminal/wait_for_exit": {
|
|
1537
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1538
|
+
const exit = await this.terminalServer.waitForExit(terminalId);
|
|
1539
|
+
await this.transport.send({ id, method: msg.method, result: exit });
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
case "terminal/kill": {
|
|
1543
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1544
|
+
this.terminalServer.kill(terminalId);
|
|
1545
|
+
await this.transport.send({ id, method: msg.method, result: {} });
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
case "terminal/release": {
|
|
1549
|
+
const terminalId = String(params.terminalId ?? "");
|
|
1550
|
+
this.terminalServer.release(terminalId);
|
|
1551
|
+
await this.transport.send({ id, method: msg.method, result: {} });
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
default:
|
|
1555
|
+
await this.transport.send({
|
|
1556
|
+
id,
|
|
1557
|
+
method: msg.method,
|
|
1558
|
+
error: { code: -32601, message: `unknown method: ${msg.method}` }
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1563
|
+
await this.transport.send({
|
|
1564
|
+
id,
|
|
1565
|
+
method: msg.method,
|
|
1566
|
+
error: { code: -32603, message }
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
function textContent(text) {
|
|
1572
|
+
return { type: "text", text };
|
|
1573
|
+
}
|
|
1574
|
+
function extractText(block) {
|
|
1575
|
+
if (typeof block !== "object" || block === null) return "";
|
|
1576
|
+
const b = block;
|
|
1577
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
1578
|
+
return "";
|
|
1579
|
+
}
|
|
635
1580
|
|
|
636
1581
|
// src/integration/acp-subagent-runner.ts
|
|
637
1582
|
var ACP_AGENT_COMMANDS = {
|
|
@@ -659,182 +1604,644 @@ var ACP_AGENT_COMMANDS = {
|
|
|
659
1604
|
}
|
|
660
1605
|
};
|
|
661
1606
|
async function makeACPSubagentRunner(options) {
|
|
662
|
-
const
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
await transport.start();
|
|
669
|
-
await transport.send({
|
|
670
|
-
method: "initialize",
|
|
671
|
-
id: "1",
|
|
672
|
-
params: {
|
|
673
|
-
capabilities: ["code-generation", "async-tools", "streaming", "progress"],
|
|
674
|
-
protocolVersion: "2024-11",
|
|
675
|
-
sessionId: options.role ?? "wrongstack-subagent"
|
|
676
|
-
}
|
|
677
|
-
});
|
|
678
|
-
const initResp = await transport.read();
|
|
679
|
-
if (!initResp || initResp.error) {
|
|
680
|
-
throw new Error(`ACP initialize failed: ${initResp?.error?.message ?? "no response"}`);
|
|
1607
|
+
const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
|
|
1608
|
+
const wrappedRunner = async (task, ctx) => {
|
|
1609
|
+
try {
|
|
1610
|
+
return await runner(task, ctx);
|
|
1611
|
+
} finally {
|
|
1612
|
+
stop();
|
|
681
1613
|
}
|
|
682
|
-
translator.attachToTransport({
|
|
683
|
-
onMessage: (h) => transport.onMessage(h),
|
|
684
|
-
send: (m) => transport.send(m)
|
|
685
|
-
});
|
|
686
|
-
sessionStarted = true;
|
|
687
1614
|
};
|
|
1615
|
+
return wrappedRunner;
|
|
1616
|
+
}
|
|
1617
|
+
async function makeACPSubagentRunnerWithStop(options) {
|
|
1618
|
+
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
1619
|
+
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
688
1620
|
const runner = async (task, ctx) => {
|
|
689
|
-
|
|
690
|
-
activeAbort.abort();
|
|
691
|
-
transport.stop();
|
|
692
|
-
});
|
|
693
|
-
await startSession();
|
|
694
|
-
const callId = crypto.randomUUID();
|
|
695
|
-
let toolResult = null;
|
|
696
|
-
const resultPromise = new Promise((resolve, reject) => {
|
|
697
|
-
const budgetMs = ctx.budget.limits.timeoutMs ?? 3e5;
|
|
698
|
-
const timeout = setTimeout(() => {
|
|
699
|
-
reject(new Error(`ACP task timed out for subagent ${ctx.subagentId} (${budgetMs}ms budget)`));
|
|
700
|
-
}, budgetMs);
|
|
701
|
-
transport.onMessage((msg) => {
|
|
702
|
-
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
703
|
-
clearTimeout(timeout);
|
|
704
|
-
resolve(msg);
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
ctx.signal.addEventListener("abort", () => {
|
|
708
|
-
clearTimeout(timeout);
|
|
709
|
-
reject(new Error("Task aborted by parent"));
|
|
710
|
-
});
|
|
711
|
-
});
|
|
1621
|
+
let session = null;
|
|
712
1622
|
try {
|
|
713
|
-
await
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1623
|
+
session = await ACPSession.start({
|
|
1624
|
+
command: options.command,
|
|
1625
|
+
...options.args !== void 0 ? { args: options.args } : {},
|
|
1626
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
1627
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1628
|
+
projectRoot,
|
|
1629
|
+
timeoutMs,
|
|
1630
|
+
role: options.role
|
|
720
1631
|
});
|
|
721
|
-
toolResult = await resultPromise;
|
|
722
1632
|
} catch (err) {
|
|
723
|
-
|
|
1633
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1634
|
+
}
|
|
1635
|
+
try {
|
|
1636
|
+
const result = await session.prompt(task.description, ctx.signal);
|
|
724
1637
|
return {
|
|
725
|
-
result:
|
|
726
|
-
iterations:
|
|
1638
|
+
result: result.text,
|
|
1639
|
+
iterations: 1,
|
|
727
1640
|
toolCalls: 0
|
|
728
1641
|
};
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
if (err instanceof ACPSessionError && err.kind === "aborted") {
|
|
1644
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1645
|
+
}
|
|
1646
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1647
|
+
} finally {
|
|
1648
|
+
try {
|
|
1649
|
+
await session.close();
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
729
1652
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1653
|
+
};
|
|
1654
|
+
const stop = () => {
|
|
1655
|
+
};
|
|
1656
|
+
return { runner, stop };
|
|
1657
|
+
}
|
|
1658
|
+
function acpErrorToSubagentError(err, subagentId) {
|
|
1659
|
+
if (err instanceof ACPSessionError) {
|
|
1660
|
+
const kind = mapACPKind(err.kind);
|
|
734
1661
|
return {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1662
|
+
kind,
|
|
1663
|
+
message: `${subagentId}: ${err.message}`,
|
|
1664
|
+
retryable: isRetryable(kind),
|
|
1665
|
+
cause: {
|
|
1666
|
+
name: err.name,
|
|
1667
|
+
message: err.message,
|
|
1668
|
+
...err.stack !== void 0 ? { stack: err.stack } : {}
|
|
1669
|
+
}
|
|
738
1670
|
};
|
|
1671
|
+
}
|
|
1672
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1673
|
+
return {
|
|
1674
|
+
kind: "bridge_failed",
|
|
1675
|
+
message: `${subagentId}: ${message}`,
|
|
1676
|
+
retryable: false,
|
|
1677
|
+
cause: {
|
|
1678
|
+
name: err instanceof Error ? err.name : "Error",
|
|
1679
|
+
message,
|
|
1680
|
+
...err instanceof Error && err.stack !== void 0 ? { stack: err.stack } : {}
|
|
1681
|
+
}
|
|
739
1682
|
};
|
|
740
|
-
return runner;
|
|
741
1683
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1684
|
+
function mapACPKind(acpKind) {
|
|
1685
|
+
switch (acpKind) {
|
|
1686
|
+
case "spawn_failed":
|
|
1687
|
+
case "init_failed":
|
|
1688
|
+
case "session_create_failed":
|
|
1689
|
+
case "agent_died":
|
|
1690
|
+
case "protocol_error":
|
|
1691
|
+
return "bridge_failed";
|
|
1692
|
+
case "prompt_failed":
|
|
1693
|
+
return "tool_failed";
|
|
1694
|
+
case "aborted":
|
|
1695
|
+
return "aborted_by_parent";
|
|
1696
|
+
case "closed":
|
|
1697
|
+
case "unsupported_capability":
|
|
1698
|
+
return "unknown";
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function isRetryable(kind) {
|
|
1702
|
+
switch (kind) {
|
|
1703
|
+
case "provider_5xx":
|
|
1704
|
+
case "provider_rate_limit":
|
|
1705
|
+
case "provider_timeout":
|
|
1706
|
+
case "tool_threw":
|
|
1707
|
+
case "budget_timeout":
|
|
1708
|
+
return true;
|
|
1709
|
+
default:
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/registry/agents.catalog.ts
|
|
1715
|
+
var AGENTS_CATALOG = [
|
|
1716
|
+
// ── Anthropic ────────────────────────────────────────────────────────
|
|
1717
|
+
{
|
|
1718
|
+
id: "claude-code",
|
|
1719
|
+
displayName: "Claude Code",
|
|
1720
|
+
vendor: "anthropic",
|
|
1721
|
+
probe: { command: "claude", args: ["--version"] },
|
|
1722
|
+
// Native ACP entry is gated behind the SDK adapter in early releases;
|
|
1723
|
+
// see https://agentclientprotocol.com/get-started/agents
|
|
1724
|
+
acp: { command: "claude", args: [] },
|
|
1725
|
+
supports: {
|
|
1726
|
+
loadSession: true,
|
|
1727
|
+
promptImages: true,
|
|
1728
|
+
terminal: true,
|
|
1729
|
+
fs: true
|
|
1730
|
+
},
|
|
1731
|
+
integration: "adapter",
|
|
1732
|
+
docs: "https://docs.anthropic.com/en/docs/claude-code"
|
|
1733
|
+
},
|
|
1734
|
+
// ── Google ───────────────────────────────────────────────────────────
|
|
1735
|
+
{
|
|
1736
|
+
id: "gemini-cli",
|
|
1737
|
+
displayName: "Gemini CLI",
|
|
1738
|
+
vendor: "google",
|
|
1739
|
+
probe: { command: "gemini", args: ["--version"] },
|
|
1740
|
+
acp: { command: "gemini", args: [] },
|
|
1741
|
+
supports: {
|
|
1742
|
+
loadSession: true,
|
|
1743
|
+
promptImages: true,
|
|
1744
|
+
terminal: true,
|
|
1745
|
+
fs: true
|
|
1746
|
+
},
|
|
1747
|
+
integration: "native",
|
|
1748
|
+
docs: "https://github.com/google-gemini/gemini-cli"
|
|
1749
|
+
},
|
|
1750
|
+
// ── OpenAI ───────────────────────────────────────────────────────────
|
|
1751
|
+
{
|
|
1752
|
+
id: "codex-cli",
|
|
1753
|
+
displayName: "Codex CLI",
|
|
1754
|
+
vendor: "openai",
|
|
1755
|
+
probe: { command: "codex", args: ["--version"] },
|
|
1756
|
+
acp: { command: "codex", args: [] },
|
|
1757
|
+
supports: {
|
|
1758
|
+
loadSession: false,
|
|
1759
|
+
promptImages: false,
|
|
1760
|
+
terminal: true,
|
|
1761
|
+
fs: true
|
|
1762
|
+
},
|
|
1763
|
+
integration: "adapter",
|
|
1764
|
+
docs: "https://github.com/openai/codex"
|
|
1765
|
+
},
|
|
1766
|
+
// ── GitHub ───────────────────────────────────────────────────────────
|
|
1767
|
+
{
|
|
1768
|
+
id: "copilot",
|
|
1769
|
+
displayName: "GitHub Copilot CLI",
|
|
1770
|
+
vendor: "github",
|
|
1771
|
+
probe: { command: "gh", args: ["copilot", "--help"] },
|
|
1772
|
+
acp: { command: "gh", args: ["copilot"] },
|
|
1773
|
+
supports: {
|
|
1774
|
+
loadSession: false,
|
|
1775
|
+
promptImages: false,
|
|
1776
|
+
terminal: true,
|
|
1777
|
+
fs: false
|
|
1778
|
+
},
|
|
1779
|
+
integration: "experimental",
|
|
1780
|
+
docs: "https://github.com/features/copilot/cli"
|
|
1781
|
+
},
|
|
1782
|
+
// ── Community / wrappers ─────────────────────────────────────────────
|
|
1783
|
+
{
|
|
1784
|
+
id: "cline",
|
|
1785
|
+
displayName: "Cline",
|
|
1786
|
+
vendor: "community",
|
|
1787
|
+
probe: { command: "npx", args: ["--version"] },
|
|
1788
|
+
acp: {
|
|
1789
|
+
command: "npx",
|
|
1790
|
+
args: ["-y", "@agentify/cline"]
|
|
1791
|
+
},
|
|
1792
|
+
supports: {
|
|
1793
|
+
loadSession: true,
|
|
1794
|
+
promptImages: true,
|
|
1795
|
+
terminal: true,
|
|
1796
|
+
fs: true
|
|
1797
|
+
},
|
|
1798
|
+
integration: "community",
|
|
1799
|
+
docs: "https://github.com/cline/cline"
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
id: "goose",
|
|
1803
|
+
displayName: "Goose",
|
|
1804
|
+
vendor: "community",
|
|
1805
|
+
probe: { command: "goose", args: ["--version"] },
|
|
1806
|
+
acp: { command: "goose", args: [] },
|
|
1807
|
+
supports: {
|
|
1808
|
+
loadSession: true,
|
|
1809
|
+
promptImages: true,
|
|
1810
|
+
terminal: true,
|
|
1811
|
+
fs: true
|
|
1812
|
+
},
|
|
1813
|
+
integration: "experimental",
|
|
1814
|
+
docs: "https://github.com/block/goose"
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
id: "openhands",
|
|
1818
|
+
displayName: "OpenHands",
|
|
1819
|
+
vendor: "community",
|
|
1820
|
+
probe: { command: "openhands", args: ["--version"] },
|
|
1821
|
+
acp: { command: "openhands", args: [] },
|
|
1822
|
+
supports: {
|
|
1823
|
+
loadSession: false,
|
|
1824
|
+
promptImages: true,
|
|
1825
|
+
terminal: true,
|
|
1826
|
+
fs: true
|
|
1827
|
+
},
|
|
1828
|
+
integration: "experimental",
|
|
1829
|
+
docs: "https://github.com/All-Hands-AI/OpenHands"
|
|
1830
|
+
},
|
|
1831
|
+
// ── Vendor CLIs (native binaries) ───────────────────────────────────
|
|
1832
|
+
{
|
|
1833
|
+
id: "qwen-code",
|
|
1834
|
+
displayName: "Qwen Code",
|
|
1835
|
+
vendor: "community",
|
|
1836
|
+
probe: { command: "qwen", args: ["--version"] },
|
|
1837
|
+
acp: { command: "qwen", args: [] },
|
|
1838
|
+
supports: {
|
|
1839
|
+
loadSession: false,
|
|
1840
|
+
promptImages: false,
|
|
1841
|
+
terminal: true,
|
|
1842
|
+
fs: false
|
|
1843
|
+
},
|
|
1844
|
+
integration: "experimental",
|
|
1845
|
+
docs: "https://github.com/QwenLM/Qwen3-Coder"
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
id: "kiro-cli",
|
|
1849
|
+
displayName: "Kiro CLI",
|
|
1850
|
+
vendor: "community",
|
|
1851
|
+
probe: { command: "kiro", args: ["--version"] },
|
|
1852
|
+
acp: { command: "kiro", args: [] },
|
|
1853
|
+
supports: {
|
|
1854
|
+
loadSession: false,
|
|
1855
|
+
promptImages: false,
|
|
1856
|
+
terminal: true,
|
|
1857
|
+
fs: true
|
|
1858
|
+
},
|
|
1859
|
+
integration: "experimental",
|
|
1860
|
+
docs: "https://kiro.dev"
|
|
1861
|
+
},
|
|
1862
|
+
{
|
|
1863
|
+
id: "opencode",
|
|
1864
|
+
displayName: "OpenCode",
|
|
1865
|
+
vendor: "community",
|
|
1866
|
+
probe: { command: "opencode", args: ["--version"] },
|
|
1867
|
+
acp: { command: "opencode", args: [] },
|
|
1868
|
+
supports: {
|
|
1869
|
+
loadSession: true,
|
|
1870
|
+
promptImages: true,
|
|
1871
|
+
terminal: true,
|
|
1872
|
+
fs: true
|
|
1873
|
+
},
|
|
1874
|
+
integration: "native",
|
|
1875
|
+
docs: "https://github.com/sst/opencode"
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
id: "mistral-vibe",
|
|
1879
|
+
displayName: "Mistral Vibe",
|
|
1880
|
+
vendor: "community",
|
|
1881
|
+
probe: { command: "vibe", args: ["--version"] },
|
|
1882
|
+
acp: { command: "vibe", args: [] },
|
|
1883
|
+
supports: {
|
|
1884
|
+
loadSession: false,
|
|
1885
|
+
promptImages: false,
|
|
1886
|
+
terminal: true,
|
|
1887
|
+
fs: false
|
|
1888
|
+
},
|
|
1889
|
+
integration: "experimental",
|
|
1890
|
+
docs: "https://github.com/mistralai/mistral-vibe"
|
|
1891
|
+
},
|
|
1892
|
+
{
|
|
1893
|
+
id: "cursor",
|
|
1894
|
+
displayName: "Cursor",
|
|
1895
|
+
vendor: "community",
|
|
1896
|
+
probe: { command: "cursor", args: ["--version"] },
|
|
1897
|
+
acp: { command: "cursor", args: [] },
|
|
1898
|
+
supports: {
|
|
1899
|
+
loadSession: true,
|
|
1900
|
+
promptImages: true,
|
|
1901
|
+
terminal: true,
|
|
1902
|
+
fs: true
|
|
1903
|
+
},
|
|
1904
|
+
integration: "experimental",
|
|
1905
|
+
docs: "https://cursor.com"
|
|
1906
|
+
}
|
|
1907
|
+
];
|
|
1908
|
+
function findAgentDescriptor(id) {
|
|
1909
|
+
return AGENTS_CATALOG.find((a) => a.id === id);
|
|
1910
|
+
}
|
|
1911
|
+
var PROBE_TIMEOUT_MS = 5e3;
|
|
1912
|
+
var PROBE_CACHE_MS = 5e3;
|
|
1913
|
+
async function defaultProbe(desc, timeoutMs) {
|
|
1914
|
+
const start = Date.now();
|
|
1915
|
+
return new Promise((resolve3) => {
|
|
1916
|
+
let settled = false;
|
|
1917
|
+
let stdout = "";
|
|
1918
|
+
let stderr = "";
|
|
1919
|
+
const finish = (result) => {
|
|
1920
|
+
if (settled) return;
|
|
1921
|
+
settled = true;
|
|
1922
|
+
try {
|
|
1923
|
+
child.kill();
|
|
1924
|
+
} catch {
|
|
757
1925
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1926
|
+
resolve3(result);
|
|
1927
|
+
};
|
|
1928
|
+
let child;
|
|
1929
|
+
try {
|
|
1930
|
+
child = spawn(desc.probe.command, [...desc.probe.args ?? []], {
|
|
1931
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1932
|
+
windowsHide: true,
|
|
1933
|
+
// On Windows, `claude`, `gemini`, `npx`, etc. are typically
|
|
1934
|
+
// installed as `.cmd` shims under AppData\Roaming\npm\. Node's
|
|
1935
|
+
// spawn() will not find them without shell-mode unless the
|
|
1936
|
+
// extension is present. `shell: true` resolves this for the
|
|
1937
|
+
// common case. The probe argv is always from our static
|
|
1938
|
+
// catalog, never user input, so shell-expansion is bounded.
|
|
1939
|
+
shell: process.platform === "win32"
|
|
1940
|
+
});
|
|
1941
|
+
} catch (err) {
|
|
1942
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1943
|
+
finish({ ok: false, reason: `spawn failed: ${msg}`, durationMs: 0 });
|
|
1944
|
+
return;
|
|
762
1945
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1946
|
+
const timer = setTimeout(() => {
|
|
1947
|
+
finish({ ok: false, reason: "probe timed out", durationMs: Date.now() - start });
|
|
1948
|
+
}, timeoutMs);
|
|
1949
|
+
child.stdout?.setEncoding("utf8");
|
|
1950
|
+
child.stdout?.on("data", (chunk) => {
|
|
1951
|
+
stdout += chunk;
|
|
766
1952
|
});
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
activeAbort.abort();
|
|
771
|
-
transport.stop();
|
|
772
|
-
};
|
|
773
|
-
const runner = async (task, ctx) => {
|
|
774
|
-
ctx.signal.addEventListener("abort", () => {
|
|
775
|
-
activeAbort.abort();
|
|
776
|
-
transport.stop();
|
|
1953
|
+
child.stderr?.setEncoding("utf8");
|
|
1954
|
+
child.stderr?.on("data", (chunk) => {
|
|
1955
|
+
stderr += chunk;
|
|
777
1956
|
});
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
reject(new Error(`ACP task timed out for subagent ${ctx.subagentId} (${budgetMs}ms budget)`));
|
|
785
|
-
}, budgetMs);
|
|
786
|
-
transport.onMessage((msg) => {
|
|
787
|
-
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
788
|
-
clearTimeout(timeout);
|
|
789
|
-
resolve(msg);
|
|
790
|
-
}
|
|
1957
|
+
child.on("error", (err) => {
|
|
1958
|
+
clearTimeout(timer);
|
|
1959
|
+
finish({
|
|
1960
|
+
ok: false,
|
|
1961
|
+
reason: `binary not found: ${err.message}`,
|
|
1962
|
+
durationMs: Date.now() - start
|
|
791
1963
|
});
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1964
|
+
});
|
|
1965
|
+
child.on("close", (code) => {
|
|
1966
|
+
clearTimeout(timer);
|
|
1967
|
+
const durationMs = Date.now() - start;
|
|
1968
|
+
const out = (stdout + stderr).trim();
|
|
1969
|
+
const isWindowsShellMiss = process.platform === "win32" && out.toLowerCase().includes("is not recognized");
|
|
1970
|
+
if (isWindowsShellMiss) {
|
|
1971
|
+
finish({
|
|
1972
|
+
ok: false,
|
|
1973
|
+
reason: "binary not found",
|
|
1974
|
+
durationMs
|
|
1975
|
+
});
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (out.length > 0) {
|
|
1979
|
+
finish({
|
|
1980
|
+
ok: true,
|
|
1981
|
+
version: out.split("\n")[0]?.trim() ?? "",
|
|
1982
|
+
path: desc.probe.command,
|
|
1983
|
+
durationMs
|
|
1984
|
+
});
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
finish({
|
|
1988
|
+
ok: false,
|
|
1989
|
+
reason: `exit code ${code ?? "null"}; no output`,
|
|
1990
|
+
durationMs
|
|
795
1991
|
});
|
|
796
1992
|
});
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
var EnsembleRegistry = class {
|
|
1996
|
+
catalog;
|
|
1997
|
+
timeoutMs;
|
|
1998
|
+
probe;
|
|
1999
|
+
cache = null;
|
|
2000
|
+
constructor(options = {}) {
|
|
2001
|
+
this.catalog = options.catalog ?? AGENTS_CATALOG;
|
|
2002
|
+
this.timeoutMs = options.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
|
|
2003
|
+
this.probe = options.probeFn ?? ((d) => defaultProbe(d, this.timeoutMs));
|
|
2004
|
+
}
|
|
2005
|
+
/** Return the full catalog (no probe), in catalog order. */
|
|
2006
|
+
listAll() {
|
|
2007
|
+
return this.catalog;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Probe every catalog entry in parallel and return the detection
|
|
2011
|
+
* results. Results are cached for `PROBE_CACHE_MS`.
|
|
2012
|
+
*/
|
|
2013
|
+
async list() {
|
|
2014
|
+
if (this.cache && Date.now() - this.cache.at < PROBE_CACHE_MS) {
|
|
2015
|
+
return this.cache.result;
|
|
2016
|
+
}
|
|
2017
|
+
const result = await Promise.all(
|
|
2018
|
+
this.catalog.map((d) => this.detect(d))
|
|
2019
|
+
);
|
|
2020
|
+
this.cache = { at: Date.now(), result };
|
|
2021
|
+
return result;
|
|
2022
|
+
}
|
|
2023
|
+
/** Probe a single descriptor. Always returns a `DetectedAgent`. */
|
|
2024
|
+
async detect(desc) {
|
|
2025
|
+
const result = await this.probe(desc);
|
|
2026
|
+
if (result.ok) {
|
|
2027
|
+
const detected = {
|
|
2028
|
+
...desc,
|
|
2029
|
+
installed: true,
|
|
2030
|
+
version: result.version
|
|
2031
|
+
};
|
|
2032
|
+
if (result.path !== void 0) detected.path = result.path;
|
|
2033
|
+
return detected;
|
|
2034
|
+
}
|
|
2035
|
+
return { ...desc, installed: false, reason: result.reason };
|
|
2036
|
+
}
|
|
2037
|
+
/** Invalidate the per-process cache. */
|
|
2038
|
+
invalidate() {
|
|
2039
|
+
this.cache = null;
|
|
2040
|
+
}
|
|
2041
|
+
/** Convenience: just the installed agents. */
|
|
2042
|
+
async listInstalled() {
|
|
2043
|
+
const all = await this.list();
|
|
2044
|
+
return all.filter((a) => a.installed);
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
var defaultEnsembleCmdResolver = (id) => {
|
|
2048
|
+
const fromMap = ACP_AGENT_COMMANDS[id];
|
|
2049
|
+
if (fromMap) return fromMap;
|
|
2050
|
+
const desc = findAgentDescriptor(id);
|
|
2051
|
+
if (!desc) return null;
|
|
2052
|
+
const out = {
|
|
2053
|
+
command: desc.acp.command,
|
|
2054
|
+
args: [...desc.acp.args ?? []],
|
|
2055
|
+
role: id
|
|
2056
|
+
};
|
|
2057
|
+
if (desc.acp.env) out.env = desc.acp.env;
|
|
2058
|
+
return out;
|
|
2059
|
+
};
|
|
2060
|
+
function setResult(results, agentId, patch) {
|
|
2061
|
+
const i = results.findIndex((r) => r.agentId === agentId);
|
|
2062
|
+
if (i < 0) return;
|
|
2063
|
+
const current = results[i];
|
|
2064
|
+
results[i] = { ...current, ...patch };
|
|
2065
|
+
}
|
|
2066
|
+
async function runOne(agentId, cmd, task, timeoutMs, signal) {
|
|
2067
|
+
const startedAt = Date.now();
|
|
2068
|
+
try {
|
|
2069
|
+
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
2070
|
+
...cmd,
|
|
2071
|
+
timeoutMs
|
|
2072
|
+
});
|
|
797
2073
|
try {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
task: task.description,
|
|
803
|
-
sessionId: ctx.subagentId
|
|
804
|
-
}
|
|
2074
|
+
const budget = new SubagentBudget({
|
|
2075
|
+
timeoutMs,
|
|
2076
|
+
maxIterations: 2e3,
|
|
2077
|
+
maxToolCalls: 5e3
|
|
805
2078
|
});
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
2079
|
+
const result = await runner(
|
|
2080
|
+
{ id: `ensemble-${agentId}`, description: task },
|
|
2081
|
+
{
|
|
2082
|
+
subagentId: agentId,
|
|
2083
|
+
config: {
|
|
2084
|
+
id: agentId,
|
|
2085
|
+
name: agentId,
|
|
2086
|
+
role: agentId,
|
|
2087
|
+
provider: "acp",
|
|
2088
|
+
prompt: ""
|
|
2089
|
+
},
|
|
2090
|
+
budget,
|
|
2091
|
+
signal: signal ?? new AbortController().signal,
|
|
2092
|
+
bridge: null
|
|
2093
|
+
}
|
|
2094
|
+
);
|
|
809
2095
|
return {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
2096
|
+
status: "success",
|
|
2097
|
+
result: result.result == null ? "" : String(result.result),
|
|
2098
|
+
durationMs: Date.now() - startedAt,
|
|
2099
|
+
iterations: result.iterations,
|
|
2100
|
+
toolCalls: result.toolCalls
|
|
813
2101
|
};
|
|
2102
|
+
} finally {
|
|
2103
|
+
try {
|
|
2104
|
+
stop();
|
|
2105
|
+
} catch {
|
|
2106
|
+
}
|
|
814
2107
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const parsed = parseToolResponse(task.id, ctx.subagentId, toolResult);
|
|
2108
|
+
} catch (err) {
|
|
2109
|
+
const e = err;
|
|
2110
|
+
const isAbort = e?.name === "AbortError" || e?.kind === "aborted" || e?.kind === "aborted_by_parent" || e?.message?.toLowerCase().includes("aborted");
|
|
819
2111
|
return {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
2112
|
+
status: isAbort ? "cancelled" : "failed",
|
|
2113
|
+
error: {
|
|
2114
|
+
kind: e?.kind ?? (isAbort ? "aborted" : "unknown"),
|
|
2115
|
+
message: e?.message ?? (err instanceof Error ? err.message : String(err))
|
|
2116
|
+
},
|
|
2117
|
+
durationMs: Date.now() - startedAt,
|
|
2118
|
+
iterations: 0,
|
|
2119
|
+
toolCalls: 0
|
|
823
2120
|
};
|
|
824
|
-
}
|
|
825
|
-
return { runner, stop };
|
|
2121
|
+
}
|
|
826
2122
|
}
|
|
827
|
-
function
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
2123
|
+
async function runEnsemble(opts) {
|
|
2124
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
2125
|
+
const registry = opts.registry ?? new EnsembleRegistry();
|
|
2126
|
+
const resolveCmd = opts.resolveCmd ?? defaultEnsembleCmdResolver;
|
|
2127
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2128
|
+
const requested = [];
|
|
2129
|
+
for (const raw of opts.agentIds.split(",")) {
|
|
2130
|
+
const id = raw.trim();
|
|
2131
|
+
if (!id || seen.has(id)) continue;
|
|
2132
|
+
seen.add(id);
|
|
2133
|
+
requested.push(id);
|
|
2134
|
+
}
|
|
2135
|
+
const results = requested.map((agentId) => ({
|
|
2136
|
+
agentId,
|
|
2137
|
+
status: "skipped",
|
|
2138
|
+
durationMs: 0,
|
|
2139
|
+
iterations: 0,
|
|
2140
|
+
toolCalls: 0
|
|
2141
|
+
}));
|
|
2142
|
+
const startMs = Date.now();
|
|
2143
|
+
if (requested.length === 0) {
|
|
2144
|
+
return {
|
|
2145
|
+
task: opts.task,
|
|
2146
|
+
requested,
|
|
2147
|
+
results,
|
|
2148
|
+
summary: { succeeded: 0, failed: 0, skipped: 0, cancelled: 0 },
|
|
2149
|
+
totalDurationMs: 0
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
const detected = await registry.list();
|
|
2153
|
+
const detectedById = new Map(detected.map((a) => [a.id, a]));
|
|
2154
|
+
const runnable = [];
|
|
2155
|
+
for (const id of requested) {
|
|
2156
|
+
const det = detectedById.get(id);
|
|
2157
|
+
if (!det || !det.installed) {
|
|
2158
|
+
setResult(results, id, {
|
|
2159
|
+
status: "skipped",
|
|
2160
|
+
reason: det?.reason ?? "not in catalog"
|
|
2161
|
+
});
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
const cmd = resolveCmd(id);
|
|
2165
|
+
if (!cmd) {
|
|
2166
|
+
setResult(results, id, {
|
|
2167
|
+
status: "failed",
|
|
2168
|
+
error: { kind: "unknown_agent", message: `Unknown ACP agent: ${id}` },
|
|
2169
|
+
durationMs: 0
|
|
2170
|
+
});
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
runnable.push({ id, cmd });
|
|
2174
|
+
}
|
|
2175
|
+
await Promise.allSettled(
|
|
2176
|
+
runnable.map(async ({ id, cmd }) => {
|
|
2177
|
+
if (opts.signal?.aborted) {
|
|
2178
|
+
setResult(results, id, {
|
|
2179
|
+
status: "cancelled",
|
|
2180
|
+
error: { kind: "aborted", message: "aborted by parent" },
|
|
2181
|
+
durationMs: 0
|
|
2182
|
+
});
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const outcome = await runOne(id, cmd, opts.task, timeoutMs, opts.signal);
|
|
2186
|
+
setResult(results, id, outcome);
|
|
2187
|
+
})
|
|
2188
|
+
);
|
|
2189
|
+
const summary = { succeeded: 0, failed: 0, skipped: 0, cancelled: 0 };
|
|
2190
|
+
for (const r of results) {
|
|
2191
|
+
if (r.status === "success") summary.succeeded++;
|
|
2192
|
+
else if (r.status === "failed") summary.failed++;
|
|
2193
|
+
else if (r.status === "cancelled") summary.cancelled++;
|
|
2194
|
+
else summary.skipped++;
|
|
2195
|
+
}
|
|
2196
|
+
return {
|
|
2197
|
+
task: opts.task,
|
|
2198
|
+
requested,
|
|
2199
|
+
results,
|
|
2200
|
+
summary,
|
|
2201
|
+
totalDurationMs: Date.now() - startMs
|
|
831
2202
|
};
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
2203
|
+
}
|
|
2204
|
+
function renderEnsembleText(result) {
|
|
2205
|
+
const lines = [];
|
|
2206
|
+
if (result.requested.length === 0) {
|
|
2207
|
+
lines.push("No agent ids provided.");
|
|
2208
|
+
return lines.join("\n");
|
|
2209
|
+
}
|
|
2210
|
+
for (const r of result.results) {
|
|
2211
|
+
lines.push(`
|
|
2212
|
+
=== ${r.agentId} ===`);
|
|
2213
|
+
switch (r.status) {
|
|
2214
|
+
case "success":
|
|
2215
|
+
lines.push(r.result && r.result.length > 0 ? r.result : "(no result)");
|
|
2216
|
+
lines.push(
|
|
2217
|
+
`[${r.agentId}] success ${r.durationMs}ms iterations=${r.iterations} toolCalls=${r.toolCalls}`
|
|
2218
|
+
);
|
|
2219
|
+
break;
|
|
2220
|
+
case "failed":
|
|
2221
|
+
lines.push(
|
|
2222
|
+
`[${r.error?.kind ?? "unknown"}] ${r.error?.message ?? "failed"}`
|
|
2223
|
+
);
|
|
2224
|
+
lines.push(`[${r.agentId}] failed ${r.durationMs}ms`);
|
|
2225
|
+
break;
|
|
2226
|
+
case "cancelled":
|
|
2227
|
+
lines.push(
|
|
2228
|
+
`[${r.error?.kind ?? "aborted"}] ${r.error?.message ?? "cancelled"}`
|
|
2229
|
+
);
|
|
2230
|
+
lines.push(`[${r.agentId}] cancelled ${r.durationMs}ms`);
|
|
2231
|
+
break;
|
|
2232
|
+
case "skipped":
|
|
2233
|
+
lines.push(`(skipped \u2014 ${r.reason ?? "not installed"})`);
|
|
2234
|
+
break;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
const { succeeded, failed, skipped, cancelled } = result.summary;
|
|
2238
|
+
lines.push(
|
|
2239
|
+
`
|
|
2240
|
+
Ensemble summary: ${succeeded} succeeded, ${failed} failed, ${cancelled} cancelled, ${skipped} skipped. (${result.totalDurationMs}ms total)`
|
|
2241
|
+
);
|
|
2242
|
+
return lines.join("\n");
|
|
836
2243
|
}
|
|
837
2244
|
|
|
838
|
-
export { ACPProtocolHandler, ACPToolsRegistry, ACP_AGENT_COMMANDS, ClientTransport, StdioTransport, ToolTranslator, WrongStackACPServer, makeACPSubagentRunner, makeACPSubagentRunnerWithStop };
|
|
2245
|
+
export { ACPProtocolHandler, ACPSession, ACPSessionError, ACPToolsRegistry, ACP_AGENT_COMMANDS, AGENTS_CATALOG, ClientTransport, EnsembleRegistry, FileServer, FsError, StdioTransport, TerminalServer, ToolTranslator, WrongStackACPServer, defaultEnsembleCmdResolver, defaultPermissionPolicy, findAgentDescriptor, makeACPSubagentRunner, makeACPSubagentRunnerWithStop, renderEnsembleText, runEnsemble };
|
|
839
2246
|
//# sourceMappingURL=index.js.map
|
|
840
2247
|
//# sourceMappingURL=index.js.map
|