@wrongstack/acp 0.274.0 → 0.275.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/README.md +376 -0
- package/dist/acp-subagent-runner-BAlo23L-.d.ts +644 -0
- package/dist/acp-v1-BxskPsdo.d.ts +520 -0
- package/dist/agent.d.ts +34 -61
- package/dist/agent.js +796 -32
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +3 -2
- package/dist/client.js +779 -112
- package/dist/client.js.map +1 -1
- package/dist/index-DEEYyEpu.d.ts +54 -0
- package/dist/index.d.ts +186 -227
- package/dist/index.js +1881 -286
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +12 -0
- package/dist/sdk.js +3350 -0
- package/dist/sdk.js.map +1 -0
- package/dist/server-agent-turn-C3U0lhA-.d.ts +163 -0
- package/dist/terminal-server-P9KpMZTT.d.ts +99 -0
- package/dist/{tools-registry-BCf8evEG.d.ts → tools-registry-D2xdbzN7.d.ts} +1 -1
- package/dist/wrongstack-acp-agent-nzrqmJnc.d.ts +341 -0
- package/dist/wrongstack-acp-agent.d.ts +2 -2
- package/dist/wrongstack-acp-agent.js +426 -26
- package/dist/wrongstack-acp-agent.js.map +1 -1
- package/package.json +7 -2
- package/dist/index-BvPqJHhm.d.ts +0 -119
- package/dist/stdio-transport-CsFr8JzC.d.ts +0 -205
- package/dist/wrongstack-acp-agent-Dv-A0bEm.d.ts +0 -310
package/dist/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { expectDefined, writeErr } from '@wrongstack/core';
|
|
|
2
2
|
import * as fsp from 'fs/promises';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
|
+
import '@wrongstack/core/coordination';
|
|
5
6
|
|
|
6
7
|
// src/agent/stdio-transport.ts
|
|
7
8
|
var ClientTransport = class {
|
|
@@ -20,9 +21,10 @@ var ClientTransport = class {
|
|
|
20
21
|
}
|
|
21
22
|
async start() {
|
|
22
23
|
if (this.child) return;
|
|
23
|
-
const [{ spawn: spawn2 }, { buildChildEnv }] = await Promise.all([
|
|
24
|
+
const [{ spawn: spawn2 }, { buildChildEnv }, os] = await Promise.all([
|
|
24
25
|
import('child_process'),
|
|
25
|
-
import('@wrongstack/core')
|
|
26
|
+
import('@wrongstack/core'),
|
|
27
|
+
import('os')
|
|
26
28
|
]);
|
|
27
29
|
return new Promise((resolve3, reject) => {
|
|
28
30
|
const timeout = setTimeout(() => {
|
|
@@ -30,10 +32,12 @@ var ClientTransport = class {
|
|
|
30
32
|
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
31
33
|
);
|
|
32
34
|
}, this.opts.handshakeTimeoutMs);
|
|
35
|
+
const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
|
|
36
|
+
const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
|
|
33
37
|
try {
|
|
34
38
|
this.child = spawn2(this.opts.command, this.opts.args ?? [], {
|
|
35
39
|
env: { ...buildChildEnv(), ...this.opts.env },
|
|
36
|
-
cwd:
|
|
40
|
+
cwd: spawnCwd,
|
|
37
41
|
stdio: ["pipe", "pipe", "pipe"],
|
|
38
42
|
windowsHide: true,
|
|
39
43
|
// On Windows, most ACP-supporting tools (claude, gemini, codex,
|
|
@@ -52,17 +56,39 @@ var ClientTransport = class {
|
|
|
52
56
|
}
|
|
53
57
|
const child = this.child;
|
|
54
58
|
child.stdout.setEncoding("utf8");
|
|
59
|
+
let settled = false;
|
|
60
|
+
const onSpawnFailure = (err) => {
|
|
61
|
+
if (settled) {
|
|
62
|
+
this.closed = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
settled = true;
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
reject(err);
|
|
68
|
+
};
|
|
69
|
+
child.on("error", onSpawnFailure);
|
|
70
|
+
child.stdout.on("error", onSpawnFailure);
|
|
71
|
+
if (this.opts.skipHandshakeMarker) {
|
|
72
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
73
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
74
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
75
|
+
child.once("spawn", () => {
|
|
76
|
+
if (settled) return;
|
|
77
|
+
settled = true;
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
resolve3();
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
55
83
|
const onReady = () => {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
settled = true;
|
|
56
86
|
child.stdout.on("data", (c) => this.onChildData(c));
|
|
57
87
|
child.stderr.on("data", (c) => this.onChildError(c));
|
|
58
88
|
child.on("close", (code) => this.onChildClose(code));
|
|
59
89
|
clearTimeout(timeout);
|
|
60
90
|
resolve3();
|
|
61
91
|
};
|
|
62
|
-
if (this.opts.skipHandshakeMarker) {
|
|
63
|
-
onReady();
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
92
|
const waitForMarker = (chunk) => {
|
|
67
93
|
this.buffer += chunk;
|
|
68
94
|
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
@@ -73,14 +99,6 @@ var ClientTransport = class {
|
|
|
73
99
|
}
|
|
74
100
|
};
|
|
75
101
|
child.stdout.on("data", waitForMarker);
|
|
76
|
-
child.stdout.on("error", (err) => {
|
|
77
|
-
clearTimeout(timeout);
|
|
78
|
-
reject(err);
|
|
79
|
-
});
|
|
80
|
-
child.on("error", (err) => {
|
|
81
|
-
clearTimeout(timeout);
|
|
82
|
-
reject(err);
|
|
83
|
-
});
|
|
84
102
|
});
|
|
85
103
|
}
|
|
86
104
|
send(msg) {
|
|
@@ -153,6 +171,115 @@ var ClientTransport = class {
|
|
|
153
171
|
}
|
|
154
172
|
}
|
|
155
173
|
};
|
|
174
|
+
|
|
175
|
+
// src/client/websocket-transport.ts
|
|
176
|
+
var WebSocketClientTransport = class {
|
|
177
|
+
ws = null;
|
|
178
|
+
handlers = /* @__PURE__ */ new Set();
|
|
179
|
+
closed = false;
|
|
180
|
+
opts;
|
|
181
|
+
constructor(opts) {
|
|
182
|
+
this.opts = opts;
|
|
183
|
+
}
|
|
184
|
+
start() {
|
|
185
|
+
const WS = globalThis.WebSocket;
|
|
186
|
+
if (!WS) {
|
|
187
|
+
return Promise.reject(
|
|
188
|
+
new Error(
|
|
189
|
+
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
194
|
+
return new Promise((resolve3, reject) => {
|
|
195
|
+
let settled = false;
|
|
196
|
+
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
197
|
+
this.ws = ws;
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
if (settled) return;
|
|
200
|
+
settled = true;
|
|
201
|
+
try {
|
|
202
|
+
ws.close();
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
206
|
+
}, timeoutMs);
|
|
207
|
+
ws.addEventListener("open", () => {
|
|
208
|
+
if (settled) return;
|
|
209
|
+
settled = true;
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
resolve3();
|
|
212
|
+
});
|
|
213
|
+
ws.addEventListener("error", (ev) => {
|
|
214
|
+
if (settled) {
|
|
215
|
+
this.closed = true;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
settled = true;
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
221
|
+
reject(new Error(message));
|
|
222
|
+
});
|
|
223
|
+
ws.addEventListener("close", () => {
|
|
224
|
+
this.closed = true;
|
|
225
|
+
});
|
|
226
|
+
ws.addEventListener("message", (ev) => {
|
|
227
|
+
this.onData(ev.data);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
send(msg) {
|
|
232
|
+
if (this.closed || !this.ws) {
|
|
233
|
+
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
this.ws.send(JSON.stringify(msg));
|
|
237
|
+
return Promise.resolve();
|
|
238
|
+
} catch (err) {
|
|
239
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
onMessage(handler) {
|
|
243
|
+
this.handlers.add(handler);
|
|
244
|
+
return () => this.handlers.delete(handler);
|
|
245
|
+
}
|
|
246
|
+
stop() {
|
|
247
|
+
this.closed = true;
|
|
248
|
+
if (this.ws) {
|
|
249
|
+
try {
|
|
250
|
+
this.ws.close();
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
this.ws = null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
onData(data) {
|
|
257
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
258
|
+
if (!text.trim()) return;
|
|
259
|
+
let msg;
|
|
260
|
+
try {
|
|
261
|
+
msg = JSON.parse(text);
|
|
262
|
+
} catch {
|
|
263
|
+
for (const line of text.split("\n")) {
|
|
264
|
+
if (!line.trim()) continue;
|
|
265
|
+
try {
|
|
266
|
+
this.dispatch(JSON.parse(line));
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.dispatch(msg);
|
|
273
|
+
}
|
|
274
|
+
dispatch(msg) {
|
|
275
|
+
for (const handler of [...this.handlers]) {
|
|
276
|
+
try {
|
|
277
|
+
handler(msg);
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
156
283
|
var DEFAULT_OPTIONS = {
|
|
157
284
|
asyncTools: true,
|
|
158
285
|
pollIntervalMs: 500,
|
|
@@ -195,6 +322,7 @@ var ToolTranslator = class {
|
|
|
195
322
|
*/
|
|
196
323
|
async callTool(transport, name, args, callId = crypto.randomUUID()) {
|
|
197
324
|
await transport.send({
|
|
325
|
+
jsonrpc: "2.0",
|
|
198
326
|
method: "tools/call",
|
|
199
327
|
id: callId,
|
|
200
328
|
params: { name, arguments: args }
|
|
@@ -318,12 +446,11 @@ function randomHex(bytes) {
|
|
|
318
446
|
}
|
|
319
447
|
|
|
320
448
|
// src/client/permission.ts
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const ranked = [...req.options].sort((a, b) => {
|
|
449
|
+
function pickAllow(options) {
|
|
450
|
+
const ranked = [...options].sort((a, b) => {
|
|
324
451
|
const score = (k) => {
|
|
325
|
-
if (k === "
|
|
326
|
-
if (k === "
|
|
452
|
+
if (k === "allow_once") return 0;
|
|
453
|
+
if (k === "allow_always") return 1;
|
|
327
454
|
if (k === "reject_once") return 2;
|
|
328
455
|
return 3;
|
|
329
456
|
};
|
|
@@ -334,7 +461,33 @@ var defaultPermissionPolicy = async (req) => {
|
|
|
334
461
|
return { outcome: "cancelled" };
|
|
335
462
|
}
|
|
336
463
|
return { outcome: "selected", optionId: chosen.optionId };
|
|
464
|
+
}
|
|
465
|
+
function pickReject(options) {
|
|
466
|
+
const reject = options.find(
|
|
467
|
+
(o) => o.kind === "reject_once" || o.kind === "reject_always"
|
|
468
|
+
);
|
|
469
|
+
return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
|
|
470
|
+
}
|
|
471
|
+
var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
|
|
472
|
+
var defaultPermissionPolicy = async (req) => {
|
|
473
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
474
|
+
return pickAllow(req.options);
|
|
337
475
|
};
|
|
476
|
+
var readOnlyPermissionPolicy = async (req) => {
|
|
477
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
478
|
+
const kind = req.toolCall.kind;
|
|
479
|
+
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
480
|
+
return pickAllow(req.options);
|
|
481
|
+
}
|
|
482
|
+
return pickReject(req.options);
|
|
483
|
+
};
|
|
484
|
+
function makePermissionPolicy(decide) {
|
|
485
|
+
return async (req) => {
|
|
486
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
487
|
+
const allow = await decide(req);
|
|
488
|
+
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
489
|
+
};
|
|
490
|
+
}
|
|
338
491
|
var TerminalServer = class {
|
|
339
492
|
terminals = /* @__PURE__ */ new Map();
|
|
340
493
|
projectRoot;
|
|
@@ -530,6 +683,12 @@ var ACPSession = class _ACPSession {
|
|
|
530
683
|
nextId = 1;
|
|
531
684
|
/** True after close() has been called. */
|
|
532
685
|
closed = false;
|
|
686
|
+
// Agent-provided info from the initialize handshake
|
|
687
|
+
agentCapabilities = {};
|
|
688
|
+
agentInfo = null;
|
|
689
|
+
authMethods = [];
|
|
690
|
+
/** Protocol version negotiated with the agent during initialize. */
|
|
691
|
+
negotiatedVersion = ACP_PROTOCOL_VERSION;
|
|
533
692
|
constructor(opts, transport) {
|
|
534
693
|
this.opts = opts;
|
|
535
694
|
this.transport = transport;
|
|
@@ -551,6 +710,36 @@ var ACPSession = class _ACPSession {
|
|
|
551
710
|
this.terminalServer = new TerminalServer(termOpts);
|
|
552
711
|
this.permissionPolicy = opts.permissionPolicy ?? defaultPermissionPolicy;
|
|
553
712
|
}
|
|
713
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
714
|
+
// Public accessors
|
|
715
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
716
|
+
/** Agent capabilities advertised during initialize. */
|
|
717
|
+
getCapabilities() {
|
|
718
|
+
return { ...this.agentCapabilities };
|
|
719
|
+
}
|
|
720
|
+
/** Authentication methods advertised by the agent. */
|
|
721
|
+
getAuthMethods() {
|
|
722
|
+
return [...this.authMethods];
|
|
723
|
+
}
|
|
724
|
+
/** Agent info (name, title, version) from initialize. */
|
|
725
|
+
getAgentInfo() {
|
|
726
|
+
return this.agentInfo;
|
|
727
|
+
}
|
|
728
|
+
/** Whether the agent requires authentication (has auth methods). */
|
|
729
|
+
requiresAuth() {
|
|
730
|
+
return this.authMethods.length > 0;
|
|
731
|
+
}
|
|
732
|
+
/** Current session id, if one exists. */
|
|
733
|
+
getSessionId() {
|
|
734
|
+
return this.sessionId;
|
|
735
|
+
}
|
|
736
|
+
/** Protocol version negotiated during initialize. */
|
|
737
|
+
getNegotiatedVersion() {
|
|
738
|
+
return this.negotiatedVersion;
|
|
739
|
+
}
|
|
740
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
741
|
+
// Lifecycle — start
|
|
742
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
554
743
|
/**
|
|
555
744
|
* Spawn the child, run the initialize handshake, install the
|
|
556
745
|
* message dispatch, and return a ready session.
|
|
@@ -560,20 +749,37 @@ var ACPSession = class _ACPSession {
|
|
|
560
749
|
command: opts.command,
|
|
561
750
|
args: opts.args ? [...opts.args] : [],
|
|
562
751
|
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
752
|
skipHandshakeMarker: true
|
|
568
753
|
};
|
|
569
754
|
if (opts.env !== void 0) transportOpts.env = opts.env;
|
|
570
755
|
if (opts.cwd !== void 0) transportOpts.cwd = opts.cwd;
|
|
571
756
|
const transport = new ClientTransport(transportOpts);
|
|
757
|
+
return _ACPSession.attach(opts, transport, `failed to spawn ${opts.command}`);
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Connect to a REMOTE ACP agent over a WebSocket instead of spawning a
|
|
761
|
+
* local subprocess. `opts.command` is ignored for the wire (a label is
|
|
762
|
+
* still useful for `role`); everything else (projectRoot sandbox for
|
|
763
|
+
* fs/terminal, timeouts, permission policy, MCP servers) applies the same.
|
|
764
|
+
*/
|
|
765
|
+
static async connectWebSocket(wsOpts, opts) {
|
|
766
|
+
const transport = new WebSocketClientTransport(wsOpts);
|
|
767
|
+
return _ACPSession.attach(opts, transport, `failed to connect to ${wsOpts.url}`);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Connect using a caller-supplied transport. Lets advanced callers plug
|
|
771
|
+
* in their own wire (SDK streams, in-process pipes, test doubles).
|
|
772
|
+
*/
|
|
773
|
+
static async connect(transport, opts) {
|
|
774
|
+
return _ACPSession.attach(opts, transport, "failed to connect transport");
|
|
775
|
+
}
|
|
776
|
+
/** Shared connect path: start the transport, install dispatch, handshake. */
|
|
777
|
+
static async attach(opts, transport, spawnErrLabel) {
|
|
572
778
|
try {
|
|
573
779
|
await transport.start();
|
|
574
780
|
} catch (err) {
|
|
575
781
|
const msg = err instanceof Error ? err.message : String(err);
|
|
576
|
-
throw new ACPSessionError("spawn_failed",
|
|
782
|
+
throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
|
|
577
783
|
}
|
|
578
784
|
const session = new _ACPSession(opts, transport);
|
|
579
785
|
transport.onMessage((msg) => session.handleMessage(msg));
|
|
@@ -588,14 +794,16 @@ var ACPSession = class _ACPSession {
|
|
|
588
794
|
}
|
|
589
795
|
return session;
|
|
590
796
|
}
|
|
797
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
798
|
+
// Initialization
|
|
799
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
591
800
|
async initialize() {
|
|
592
801
|
const id = this.allocId();
|
|
593
802
|
const result = await this.sendRequest(id, "initialize", {
|
|
594
803
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
595
804
|
clientCapabilities: {
|
|
596
805
|
fs: { readTextFile: true, writeTextFile: true },
|
|
597
|
-
terminal: true
|
|
598
|
-
promptCapabilities: { image: false, audio: false, embeddedContext: true }
|
|
806
|
+
terminal: true
|
|
599
807
|
},
|
|
600
808
|
clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.263.0" }
|
|
601
809
|
});
|
|
@@ -606,53 +814,344 @@ var ACPSession = class _ACPSession {
|
|
|
606
814
|
throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
|
|
607
815
|
}
|
|
608
816
|
const r = result;
|
|
609
|
-
if (r.protocolVersion
|
|
817
|
+
if (r.protocolVersion > ACP_PROTOCOL_VERSION) {
|
|
818
|
+
throw new ACPSessionError(
|
|
819
|
+
"unsupported_capability",
|
|
820
|
+
`agent requires protocolVersion=${r.protocolVersion}, client supports up to ${ACP_PROTOCOL_VERSION}`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
this.negotiatedVersion = r.protocolVersion;
|
|
824
|
+
this.agentCapabilities = r.agentCapabilities ?? {};
|
|
825
|
+
this.agentInfo = r.agentInfo ?? null;
|
|
826
|
+
this.authMethods = r.authMethods ?? [];
|
|
827
|
+
this.state = "ready";
|
|
828
|
+
}
|
|
829
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
830
|
+
// Authentication
|
|
831
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
832
|
+
/**
|
|
833
|
+
* Authenticate with the agent using one of the advertised auth methods.
|
|
834
|
+
* Call this AFTER start() and BEFORE any session/new call.
|
|
835
|
+
*
|
|
836
|
+
* Throws ACPSessionError('auth_failed') if the agent rejects the
|
|
837
|
+
* authentication or if the methodId is not in the advertised list.
|
|
838
|
+
*/
|
|
839
|
+
async authenticate(methodId) {
|
|
840
|
+
if (this.state === "closed") {
|
|
841
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
842
|
+
}
|
|
843
|
+
if (this.state !== "ready") {
|
|
844
|
+
throw new ACPSessionError(
|
|
845
|
+
"protocol_error",
|
|
846
|
+
`authenticate called in state=${this.state} (expected 'ready')`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
850
|
+
throw new ACPSessionError(
|
|
851
|
+
"auth_failed",
|
|
852
|
+
`auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const id = this.allocId();
|
|
856
|
+
const result = await this.sendRequest(id, "authenticate", { methodId });
|
|
857
|
+
if (isJsonRpcError(result)) {
|
|
858
|
+
throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
|
|
859
|
+
}
|
|
860
|
+
this.state = "authenticated";
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Log out from the current authenticated session.
|
|
864
|
+
* Only callable if the agent advertises `auth.logout` capability.
|
|
865
|
+
*/
|
|
866
|
+
async logout() {
|
|
867
|
+
if (this.state === "closed") {
|
|
868
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
869
|
+
}
|
|
870
|
+
if (!this.agentCapabilities.auth?.logout) {
|
|
610
871
|
throw new ACPSessionError(
|
|
611
872
|
"unsupported_capability",
|
|
612
|
-
|
|
873
|
+
"agent does not support logout (auth.logout capability not advertised)"
|
|
613
874
|
);
|
|
614
875
|
}
|
|
876
|
+
const id = this.allocId();
|
|
877
|
+
const result = await this.sendRequest(id, "logout", {});
|
|
878
|
+
if (isJsonRpcError(result)) {
|
|
879
|
+
throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
|
|
880
|
+
}
|
|
615
881
|
this.state = "ready";
|
|
616
882
|
}
|
|
883
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
884
|
+
// Session management
|
|
885
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
886
|
+
/**
|
|
887
|
+
* Load an existing session. The agent replays the conversation history
|
|
888
|
+
* via session/update notifications before responding.
|
|
889
|
+
*
|
|
890
|
+
* Only works if the agent advertises `loadSession` capability.
|
|
891
|
+
*
|
|
892
|
+
* @param sessionId - The session to load
|
|
893
|
+
* @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
|
|
894
|
+
* @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
|
|
895
|
+
*/
|
|
896
|
+
async loadSession(sessionId, mcpServers, cwd) {
|
|
897
|
+
if (this.closed) {
|
|
898
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
899
|
+
}
|
|
900
|
+
if (!this.agentCapabilities.loadSession) {
|
|
901
|
+
throw new ACPSessionError(
|
|
902
|
+
"unsupported_capability",
|
|
903
|
+
"agent does not support session/load (loadSession capability not advertised)"
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (this.sessionId) {
|
|
907
|
+
await this.closeSession();
|
|
908
|
+
}
|
|
909
|
+
this.resetScratch();
|
|
910
|
+
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
911
|
+
const id = this.allocId();
|
|
912
|
+
const result = await this.sendRequest(id, "session/load", {
|
|
913
|
+
sessionId,
|
|
914
|
+
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
915
|
+
mcpServers: servers
|
|
916
|
+
});
|
|
917
|
+
if (isJsonRpcError(result)) {
|
|
918
|
+
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
919
|
+
}
|
|
920
|
+
this.sessionId = sessionId;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Resume an existing session without replaying history.
|
|
924
|
+
*
|
|
925
|
+
* Only works if the agent advertises `sessionCapabilities.resume`.
|
|
926
|
+
*
|
|
927
|
+
* @param sessionId - The session to resume
|
|
928
|
+
* @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
|
|
929
|
+
* @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
|
|
930
|
+
*/
|
|
931
|
+
async resumeSession(sessionId, mcpServers, cwd) {
|
|
932
|
+
if (this.closed) {
|
|
933
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
934
|
+
}
|
|
935
|
+
if (!this.agentCapabilities.sessionCapabilities?.resume) {
|
|
936
|
+
throw new ACPSessionError(
|
|
937
|
+
"unsupported_capability",
|
|
938
|
+
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
if (this.sessionId) {
|
|
942
|
+
await this.closeSession();
|
|
943
|
+
}
|
|
944
|
+
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
945
|
+
const id = this.allocId();
|
|
946
|
+
const result = await this.sendRequest(id, "session/resume", {
|
|
947
|
+
sessionId,
|
|
948
|
+
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
949
|
+
mcpServers: servers
|
|
950
|
+
});
|
|
951
|
+
if (isJsonRpcError(result)) {
|
|
952
|
+
throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
|
|
953
|
+
}
|
|
954
|
+
this.sessionId = sessionId;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* List existing sessions known to the agent.
|
|
958
|
+
*
|
|
959
|
+
* Only works if the agent advertises `sessionCapabilities.list`.
|
|
960
|
+
*/
|
|
961
|
+
async listSessions(cursor, cwd) {
|
|
962
|
+
if (this.closed) {
|
|
963
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
964
|
+
}
|
|
965
|
+
if (!this.agentCapabilities.sessionCapabilities?.list) {
|
|
966
|
+
throw new ACPSessionError(
|
|
967
|
+
"unsupported_capability",
|
|
968
|
+
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
const id = this.allocId();
|
|
972
|
+
const params = {};
|
|
973
|
+
if (cursor !== void 0) params.cursor = cursor;
|
|
974
|
+
if (cwd !== void 0) params.cwd = cwd;
|
|
975
|
+
const result = await this.sendRequest(id, "session/list", params);
|
|
976
|
+
if (isJsonRpcError(result)) {
|
|
977
|
+
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
978
|
+
}
|
|
979
|
+
const r = result;
|
|
980
|
+
return {
|
|
981
|
+
sessions: r.sessions ?? [],
|
|
982
|
+
nextCursor: r.nextCursor
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Delete a session from the agent's session list.
|
|
987
|
+
*
|
|
988
|
+
* Only works if the agent advertises `sessionCapabilities.delete`.
|
|
989
|
+
*/
|
|
990
|
+
async deleteSession(sessionId) {
|
|
991
|
+
if (this.closed) {
|
|
992
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
993
|
+
}
|
|
994
|
+
if (!this.agentCapabilities.sessionCapabilities?.delete) {
|
|
995
|
+
throw new ACPSessionError(
|
|
996
|
+
"unsupported_capability",
|
|
997
|
+
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const id = this.allocId();
|
|
1001
|
+
const result = await this.sendRequest(id, "session/delete", { sessionId });
|
|
1002
|
+
if (isJsonRpcError(result)) {
|
|
1003
|
+
throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
|
|
1004
|
+
}
|
|
1005
|
+
if (this.sessionId === sessionId) {
|
|
1006
|
+
this.sessionId = null;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Fork a session — create a new session from an existing one.
|
|
1011
|
+
*/
|
|
1012
|
+
async forkSession(sourceSessionId, cwd, mcpServers) {
|
|
1013
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1014
|
+
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
1015
|
+
const id = this.allocId();
|
|
1016
|
+
const result = await this.sendRequest(id, "session/fork", {
|
|
1017
|
+
sessionId: sourceSessionId,
|
|
1018
|
+
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
1019
|
+
...servers.length > 0 ? { mcpServers: servers } : {}
|
|
1020
|
+
});
|
|
1021
|
+
if (isJsonRpcError(result)) {
|
|
1022
|
+
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
1023
|
+
}
|
|
1024
|
+
const newId = result.sessionId;
|
|
1025
|
+
if (typeof newId !== "string" || !newId) {
|
|
1026
|
+
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
1027
|
+
}
|
|
1028
|
+
return newId;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Set the active mode for a session.
|
|
1032
|
+
*/
|
|
1033
|
+
async setMode(sessionId, modeId) {
|
|
1034
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1035
|
+
const id = this.allocId();
|
|
1036
|
+
const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
1037
|
+
if (isJsonRpcError(result)) {
|
|
1038
|
+
throw new ACPSessionError("prompt_failed", `session/set_mode failed: ${result.message}`, result);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Set a configuration option for a session.
|
|
1043
|
+
*/
|
|
1044
|
+
async setConfigOption(sessionId, configId, value) {
|
|
1045
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1046
|
+
const id = this.allocId();
|
|
1047
|
+
const result = await this.sendRequest(id, "session/set_config_option", {
|
|
1048
|
+
sessionId,
|
|
1049
|
+
configId,
|
|
1050
|
+
value
|
|
1051
|
+
});
|
|
1052
|
+
if (isJsonRpcError(result)) {
|
|
1053
|
+
throw new ACPSessionError("prompt_failed", `session/set_config_option failed: ${result.message}`, result);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* List available providers and the current provider.
|
|
1058
|
+
*/
|
|
1059
|
+
async listProviders() {
|
|
1060
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1061
|
+
const id = this.allocId();
|
|
1062
|
+
const result = await this.sendRequest(id, "providers/list", {});
|
|
1063
|
+
if (isJsonRpcError(result)) {
|
|
1064
|
+
throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
|
|
1065
|
+
}
|
|
1066
|
+
const r = result;
|
|
1067
|
+
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Send an MCP message to the agent for routing.
|
|
1071
|
+
*/
|
|
1072
|
+
async mcpMessage(connectionId, message) {
|
|
1073
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1074
|
+
const id = this.allocId();
|
|
1075
|
+
const result = await this.sendRequest(id, "mcp/message", { connectionId, message });
|
|
1076
|
+
if (isJsonRpcError(result)) {
|
|
1077
|
+
throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
|
|
1078
|
+
}
|
|
1079
|
+
return result;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Set the active provider for the agent.
|
|
1083
|
+
*/
|
|
1084
|
+
async setProvider(providerId, config) {
|
|
1085
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1086
|
+
const id = this.allocId();
|
|
1087
|
+
const result = await this.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
1088
|
+
if (isJsonRpcError(result)) {
|
|
1089
|
+
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Disable the current provider.
|
|
1094
|
+
*/
|
|
1095
|
+
async disableProvider() {
|
|
1096
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1097
|
+
const id = this.allocId();
|
|
1098
|
+
const result = await this.sendRequest(id, "providers/disable", {});
|
|
1099
|
+
if (isJsonRpcError(result)) {
|
|
1100
|
+
throw new ACPSessionError("prompt_failed", `providers/disable failed: ${result.message}`, result);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1104
|
+
// Prompt
|
|
1105
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
617
1106
|
/**
|
|
618
1107
|
* Run one prompt turn. Creates a session if needed, sends the
|
|
619
1108
|
* prompt, streams session/update notifications, and resolves with
|
|
620
1109
|
* the agent's response.
|
|
621
1110
|
*
|
|
1111
|
+
* @param blocks - Content blocks to send. Use `textContent()` for plain
|
|
1112
|
+
* text, or include ImageContent/AudioContent if the agent's
|
|
1113
|
+
* `promptCapabilities` allow it.
|
|
1114
|
+
* @param signal - AbortSignal for cancellation.
|
|
1115
|
+
*
|
|
622
1116
|
* Cancellation: if `signal` aborts mid-prompt, we send
|
|
623
1117
|
* `session/cancel` (a notification per spec) and keep accepting
|
|
624
1118
|
* updates until the agent returns with `stopReason: 'cancelled'`.
|
|
625
1119
|
* The result is the same shape as a normal turn, with
|
|
626
1120
|
* `stopReason === 'cancelled'`.
|
|
627
1121
|
*/
|
|
628
|
-
async prompt(
|
|
1122
|
+
async prompt(blocks, signal, onProgress) {
|
|
629
1123
|
if (this.closed) {
|
|
630
1124
|
throw new ACPSessionError("closed", "session is closed");
|
|
631
1125
|
}
|
|
632
|
-
if (this.state !== "ready" && this.state !== "done") {
|
|
1126
|
+
if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
|
|
633
1127
|
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
634
1128
|
}
|
|
635
1129
|
if (signal.aborted) {
|
|
636
|
-
return
|
|
1130
|
+
return emptyRunResult("cancelled");
|
|
637
1131
|
}
|
|
638
1132
|
if (!this.sessionId) {
|
|
639
1133
|
await this.createSession();
|
|
640
1134
|
}
|
|
641
1135
|
this.resetScratch();
|
|
1136
|
+
this.progressHandler = onProgress ?? null;
|
|
642
1137
|
const promptId = this.allocId();
|
|
643
1138
|
const turnPromise = this.sendRequest(
|
|
644
1139
|
promptId,
|
|
645
1140
|
"session/prompt",
|
|
646
1141
|
{
|
|
647
1142
|
sessionId: this.sessionId,
|
|
648
|
-
prompt:
|
|
1143
|
+
prompt: blocks
|
|
649
1144
|
},
|
|
650
1145
|
this.timeoutMs
|
|
651
1146
|
);
|
|
652
1147
|
let cancelled = false;
|
|
653
1148
|
const onAbort = () => {
|
|
654
1149
|
cancelled = true;
|
|
655
|
-
this.transport.send({
|
|
1150
|
+
this.transport.send({
|
|
1151
|
+
jsonrpc: "2.0",
|
|
1152
|
+
method: "session/cancel",
|
|
1153
|
+
params: { sessionId: this.sessionId }
|
|
1154
|
+
}).catch(() => {
|
|
656
1155
|
});
|
|
657
1156
|
};
|
|
658
1157
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -670,6 +1169,7 @@ var ACPSession = class _ACPSession {
|
|
|
670
1169
|
throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
|
|
671
1170
|
} finally {
|
|
672
1171
|
signal.removeEventListener("abort", onAbort);
|
|
1172
|
+
this.progressHandler = null;
|
|
673
1173
|
}
|
|
674
1174
|
this.state = "done";
|
|
675
1175
|
if (isJsonRpcError(response)) {
|
|
@@ -682,14 +1182,18 @@ var ACPSession = class _ACPSession {
|
|
|
682
1182
|
stopReason,
|
|
683
1183
|
hasText: finalText.length > 0,
|
|
684
1184
|
usage: this.scratch.usage,
|
|
685
|
-
plan: this.scratch.plan
|
|
1185
|
+
plan: this.scratch.plan,
|
|
1186
|
+
toolCalls: [...this.scratch.toolCalls.values()],
|
|
1187
|
+
diffs: this.scratch.diffs,
|
|
1188
|
+
thoughts: this.scratch.thoughts
|
|
686
1189
|
};
|
|
687
1190
|
}
|
|
688
1191
|
async createSession() {
|
|
1192
|
+
const servers = this.filterMcpServers(this.opts.mcpServers);
|
|
689
1193
|
const id = this.allocId();
|
|
690
1194
|
const result = await this.sendRequest(id, "session/new", {
|
|
691
1195
|
cwd: this.opts.cwd ?? this.opts.projectRoot,
|
|
692
|
-
mcpServers:
|
|
1196
|
+
mcpServers: servers
|
|
693
1197
|
});
|
|
694
1198
|
if (isJsonRpcError(result)) {
|
|
695
1199
|
throw new ACPSessionError(
|
|
@@ -708,12 +1212,40 @@ var ACPSession = class _ACPSession {
|
|
|
708
1212
|
}
|
|
709
1213
|
this.sessionId = sessionId;
|
|
710
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Close the current session gracefully (if the agent supports it).
|
|
1217
|
+
*
|
|
1218
|
+
* Sends `session/close` JSON-RPC request, then clears the local
|
|
1219
|
+
* session id. Best-effort — errors are swallowed so the caller can
|
|
1220
|
+
* always proceed to transport teardown.
|
|
1221
|
+
*/
|
|
1222
|
+
async closeSession() {
|
|
1223
|
+
if (!this.sessionId) return;
|
|
1224
|
+
const sid = this.sessionId;
|
|
1225
|
+
this.sessionId = null;
|
|
1226
|
+
if (this.agentCapabilities.sessionCapabilities?.close) {
|
|
1227
|
+
const id = this.allocId();
|
|
1228
|
+
try {
|
|
1229
|
+
await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1235
|
+
// Lifecycle — close
|
|
1236
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
711
1237
|
/** Tear down the session and kill the child process. */
|
|
712
1238
|
async close() {
|
|
713
1239
|
if (this.closed) return;
|
|
714
1240
|
this.closed = true;
|
|
715
1241
|
this.state = "closed";
|
|
716
1242
|
this.terminalServer.releaseAll();
|
|
1243
|
+
if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
|
|
1244
|
+
try {
|
|
1245
|
+
await this.closeSession();
|
|
1246
|
+
} catch {
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
717
1249
|
for (const [, p] of this.pending) {
|
|
718
1250
|
clearTimeout(p.timeoutHandle);
|
|
719
1251
|
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
@@ -724,6 +1256,24 @@ var ACPSession = class _ACPSession {
|
|
|
724
1256
|
} catch {
|
|
725
1257
|
}
|
|
726
1258
|
}
|
|
1259
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1260
|
+
// Helpers
|
|
1261
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1262
|
+
/**
|
|
1263
|
+
* Filter MCP servers according to agent capabilities.
|
|
1264
|
+
* - Stdio servers are always included.
|
|
1265
|
+
* - HTTP servers are only included if agent supports mcpCapabilities.http.
|
|
1266
|
+
* - SSE servers are only included if agent supports mcpCapabilities.sse.
|
|
1267
|
+
*/
|
|
1268
|
+
filterMcpServers(servers) {
|
|
1269
|
+
if (!servers || servers.length === 0) return [];
|
|
1270
|
+
const mcpCaps = this.agentCapabilities.mcpCapabilities ?? {};
|
|
1271
|
+
return servers.filter((s) => {
|
|
1272
|
+
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
1273
|
+
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
1274
|
+
return true;
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
727
1277
|
// ────────────────────────────────────────────────────────────────────
|
|
728
1278
|
// Wire layer
|
|
729
1279
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -757,6 +1307,28 @@ var ACPSession = class _ACPSession {
|
|
|
757
1307
|
});
|
|
758
1308
|
});
|
|
759
1309
|
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Send a JSON-RPC 2.0 success response to an agent-initiated request.
|
|
1312
|
+
*
|
|
1313
|
+
* Per JSON-RPC 2.0 (and the official ACP SDK's message router) a Response
|
|
1314
|
+
* object MUST carry `jsonrpc: "2.0"` and MUST NOT carry a `method` field —
|
|
1315
|
+
* the SDK classifies any object with a `method` key as a Request and drops
|
|
1316
|
+
* it as a response, so an agent's `fs/*`, `terminal/*`, or
|
|
1317
|
+
* `session/request_permission` callback would hang forever. The legacy
|
|
1318
|
+
* `ACPMessage` type predates v1 (requires `method`, lacks `jsonrpc`), so we
|
|
1319
|
+
* build the correct wire object and cast at the boundary.
|
|
1320
|
+
*/
|
|
1321
|
+
sendResult(id, result) {
|
|
1322
|
+
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
1323
|
+
}
|
|
1324
|
+
/** Send a JSON-RPC 2.0 error response (no `method` field, per spec). */
|
|
1325
|
+
sendErrorResponse(id, code, message) {
|
|
1326
|
+
return this.transport.send({
|
|
1327
|
+
jsonrpc: "2.0",
|
|
1328
|
+
id,
|
|
1329
|
+
error: { code, message }
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
760
1332
|
handleMessage(msg) {
|
|
761
1333
|
if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
|
|
762
1334
|
const pending = this.pending.get(msg.id);
|
|
@@ -786,6 +1358,23 @@ var ACPSession = class _ACPSession {
|
|
|
786
1358
|
void this.handleTerminalRequest(msg);
|
|
787
1359
|
return;
|
|
788
1360
|
}
|
|
1361
|
+
if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
|
|
1362
|
+
if (msg.id !== void 0) {
|
|
1363
|
+
this.sendResult(msg.id, {}).catch(() => {
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
|
|
1369
|
+
if (msg.id !== void 0) {
|
|
1370
|
+
this.sendResult(msg.id, {}).catch(() => {
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (msg.method === "$/cancel_request") {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
789
1378
|
if (msg.method) {
|
|
790
1379
|
console.warn(`[acp-session] unhandled method: ${msg.method}`);
|
|
791
1380
|
}
|
|
@@ -794,31 +1383,44 @@ var ACPSession = class _ACPSession {
|
|
|
794
1383
|
const update = msg.params?.update;
|
|
795
1384
|
if (typeof update !== "object" || update === null) return;
|
|
796
1385
|
const u = update;
|
|
1386
|
+
this.emitProgress({ type: "raw", update: u });
|
|
797
1387
|
switch (u.sessionUpdate) {
|
|
798
1388
|
case "agent_message_chunk": {
|
|
799
1389
|
const text = extractText(u.content);
|
|
800
|
-
if (text)
|
|
1390
|
+
if (text) {
|
|
1391
|
+
this.scratch.text += text;
|
|
1392
|
+
this.emitProgress({ type: "message", text });
|
|
1393
|
+
}
|
|
801
1394
|
return;
|
|
802
1395
|
}
|
|
803
|
-
case "thought_chunk":
|
|
1396
|
+
case "thought_chunk": {
|
|
1397
|
+
const text = extractText(u.content);
|
|
1398
|
+
if (text) {
|
|
1399
|
+
this.scratch.thoughts += text;
|
|
1400
|
+
this.emitProgress({ type: "thought", text });
|
|
1401
|
+
}
|
|
804
1402
|
return;
|
|
1403
|
+
}
|
|
805
1404
|
case "tool_call":
|
|
806
|
-
case "tool_call_update":
|
|
1405
|
+
case "tool_call_update": {
|
|
1406
|
+
this.captureToolCall(u, u.sessionUpdate === "tool_call");
|
|
807
1407
|
return;
|
|
1408
|
+
}
|
|
808
1409
|
case "plan":
|
|
809
1410
|
if (Array.isArray(u.entries)) {
|
|
810
|
-
this.
|
|
1411
|
+
this.scratch.plan = u.entries;
|
|
1412
|
+
this.emitProgress({ type: "plan", entries: u.entries });
|
|
811
1413
|
}
|
|
812
1414
|
return;
|
|
813
1415
|
case "usage_update":
|
|
814
1416
|
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
815
|
-
|
|
1417
|
+
const usage = {
|
|
816
1418
|
used: u.used,
|
|
817
1419
|
size: u.size,
|
|
818
|
-
...typeof u.cost === "object" && u.cost !== null ? {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
});
|
|
1420
|
+
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
1421
|
+
};
|
|
1422
|
+
this.scratch.usage = usage;
|
|
1423
|
+
this.emitProgress({ type: "usage", usage });
|
|
822
1424
|
}
|
|
823
1425
|
return;
|
|
824
1426
|
case "available_commands_update":
|
|
@@ -826,27 +1428,62 @@ var ACPSession = class _ACPSession {
|
|
|
826
1428
|
case "config_option_update":
|
|
827
1429
|
case "session_info_update":
|
|
828
1430
|
case "user_message_chunk":
|
|
1431
|
+
case "next_edit_suggestions":
|
|
1432
|
+
case "elicitation":
|
|
829
1433
|
return;
|
|
830
1434
|
default:
|
|
831
|
-
console.warn(`[acp-session] unhandled sessionUpdate: ${u.sessionUpdate}`);
|
|
832
1435
|
return;
|
|
833
1436
|
}
|
|
834
1437
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
this.scratch.
|
|
1438
|
+
/**
|
|
1439
|
+
* Fold a `tool_call` / `tool_call_update` notification into the scratch
|
|
1440
|
+
* tool-call map (deduped by toolCallId), extract any `diff` content into
|
|
1441
|
+
* the diffs list, and emit live progress.
|
|
1442
|
+
*/
|
|
1443
|
+
captureToolCall(u, isNew) {
|
|
1444
|
+
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
|
|
1445
|
+
if (!toolCallId) return;
|
|
1446
|
+
const prev = this.scratch.toolCalls.get(toolCallId);
|
|
1447
|
+
const record = {
|
|
1448
|
+
toolCallId,
|
|
1449
|
+
title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
|
|
1450
|
+
kind: typeof u.kind === "string" ? u.kind : prev?.kind,
|
|
1451
|
+
status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
|
|
1452
|
+
rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
|
|
1453
|
+
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
|
|
1454
|
+
};
|
|
1455
|
+
this.scratch.toolCalls.set(toolCallId, record);
|
|
1456
|
+
if (Array.isArray(u.content)) {
|
|
1457
|
+
for (const c of u.content) {
|
|
1458
|
+
if (c && typeof c === "object" && c.type === "diff") {
|
|
1459
|
+
const diff = {
|
|
1460
|
+
path: c.path,
|
|
1461
|
+
oldText: c.oldText,
|
|
1462
|
+
newText: c.newText
|
|
1463
|
+
};
|
|
1464
|
+
this.scratch.diffs.push(diff);
|
|
1465
|
+
this.emitProgress({ type: "diff", diff });
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
this.emitProgress({
|
|
1470
|
+
type: isNew ? "tool_call" : "tool_call_update",
|
|
1471
|
+
toolCall: record
|
|
1472
|
+
});
|
|
844
1473
|
}
|
|
845
|
-
|
|
846
|
-
this.
|
|
1474
|
+
emitProgress(event) {
|
|
1475
|
+
if (!this.progressHandler) return;
|
|
1476
|
+
try {
|
|
1477
|
+
this.progressHandler(event);
|
|
1478
|
+
} catch {
|
|
1479
|
+
}
|
|
847
1480
|
}
|
|
1481
|
+
/** Live progress handler installed for the duration of a `prompt()` turn. */
|
|
1482
|
+
progressHandler = null;
|
|
1483
|
+
// Per-prompt scratch state
|
|
1484
|
+
scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
848
1485
|
resetScratch() {
|
|
849
|
-
this.scratch = { text: "" };
|
|
1486
|
+
this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
850
1487
|
}
|
|
851
1488
|
async handlePermissionRequest(msg) {
|
|
852
1489
|
const id = msg.id;
|
|
@@ -855,11 +1492,7 @@ var ACPSession = class _ACPSession {
|
|
|
855
1492
|
const toolCall = params?.toolCall;
|
|
856
1493
|
const options = Array.isArray(params?.options) ? params.options : [];
|
|
857
1494
|
if (!toolCall) {
|
|
858
|
-
await this.
|
|
859
|
-
id,
|
|
860
|
-
method: "session/request_permission",
|
|
861
|
-
error: { code: -32602, message: "toolCall is required" }
|
|
862
|
-
});
|
|
1495
|
+
await this.sendErrorResponse(id, -32602, "toolCall is required");
|
|
863
1496
|
return;
|
|
864
1497
|
}
|
|
865
1498
|
const policyAbort = new AbortController();
|
|
@@ -868,22 +1501,14 @@ var ACPSession = class _ACPSession {
|
|
|
868
1501
|
options,
|
|
869
1502
|
signal: policyAbort.signal
|
|
870
1503
|
});
|
|
871
|
-
await this.
|
|
872
|
-
id,
|
|
873
|
-
method: "session/request_permission",
|
|
874
|
-
result: { outcome }
|
|
875
|
-
});
|
|
1504
|
+
await this.sendResult(id, { outcome });
|
|
876
1505
|
}
|
|
877
1506
|
async handleFsRequest(msg) {
|
|
878
1507
|
const id = msg.id;
|
|
879
1508
|
if (id === void 0) return;
|
|
880
1509
|
const params = msg.params;
|
|
881
1510
|
if (!params?.path) {
|
|
882
|
-
await this.
|
|
883
|
-
id,
|
|
884
|
-
method: msg.method,
|
|
885
|
-
error: { code: -32602, message: "path is required" }
|
|
886
|
-
});
|
|
1511
|
+
await this.sendErrorResponse(id, -32602, "path is required");
|
|
887
1512
|
return;
|
|
888
1513
|
}
|
|
889
1514
|
try {
|
|
@@ -892,19 +1517,19 @@ var ACPSession = class _ACPSession {
|
|
|
892
1517
|
sessionId: params.sessionId ?? "",
|
|
893
1518
|
path: params.path
|
|
894
1519
|
});
|
|
895
|
-
await this.
|
|
1520
|
+
await this.sendResult(id, result);
|
|
896
1521
|
} else {
|
|
897
1522
|
await this.fileServer.writeTextFile({
|
|
898
1523
|
sessionId: params.sessionId ?? "",
|
|
899
1524
|
path: params.path,
|
|
900
1525
|
content: params.content ?? ""
|
|
901
1526
|
});
|
|
902
|
-
await this.
|
|
1527
|
+
await this.sendResult(id, {});
|
|
903
1528
|
}
|
|
904
1529
|
} catch (err) {
|
|
905
1530
|
const code = err instanceof FsError ? -32602 : -32603;
|
|
906
1531
|
const message = err instanceof Error ? err.message : String(err);
|
|
907
|
-
await this.
|
|
1532
|
+
await this.sendErrorResponse(id, code, message);
|
|
908
1533
|
}
|
|
909
1534
|
}
|
|
910
1535
|
async handleTerminalRequest(msg) {
|
|
@@ -929,61 +1554,73 @@ var ACPSession = class _ACPSession {
|
|
|
929
1554
|
createOpts.outputByteLimit = params.outputByteLimit;
|
|
930
1555
|
}
|
|
931
1556
|
const result = this.terminalServer.create(createOpts);
|
|
932
|
-
await this.
|
|
1557
|
+
await this.sendResult(id, result);
|
|
933
1558
|
return;
|
|
934
1559
|
}
|
|
935
1560
|
case "terminal/output": {
|
|
936
1561
|
const terminalId = String(params.terminalId ?? "");
|
|
937
1562
|
const out = this.terminalServer.output(terminalId);
|
|
938
|
-
await this.
|
|
1563
|
+
await this.sendResult(id, out);
|
|
939
1564
|
return;
|
|
940
1565
|
}
|
|
941
1566
|
case "terminal/wait_for_exit": {
|
|
942
1567
|
const terminalId = String(params.terminalId ?? "");
|
|
943
1568
|
const exit = await this.terminalServer.waitForExit(terminalId);
|
|
944
|
-
await this.
|
|
1569
|
+
await this.sendResult(id, exit);
|
|
945
1570
|
return;
|
|
946
1571
|
}
|
|
947
1572
|
case "terminal/kill": {
|
|
948
1573
|
const terminalId = String(params.terminalId ?? "");
|
|
949
1574
|
this.terminalServer.kill(terminalId);
|
|
950
|
-
await this.
|
|
1575
|
+
await this.sendResult(id, {});
|
|
951
1576
|
return;
|
|
952
1577
|
}
|
|
953
1578
|
case "terminal/release": {
|
|
954
1579
|
const terminalId = String(params.terminalId ?? "");
|
|
955
1580
|
this.terminalServer.release(terminalId);
|
|
956
|
-
await this.
|
|
1581
|
+
await this.sendResult(id, {});
|
|
957
1582
|
return;
|
|
958
1583
|
}
|
|
959
1584
|
default:
|
|
960
|
-
await this.
|
|
961
|
-
id,
|
|
962
|
-
method: msg.method,
|
|
963
|
-
error: { code: -32601, message: `unknown method: ${msg.method}` }
|
|
964
|
-
});
|
|
1585
|
+
await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
965
1586
|
}
|
|
966
1587
|
} catch (err) {
|
|
967
1588
|
const message = err instanceof Error ? err.message : String(err);
|
|
968
|
-
await this.
|
|
969
|
-
id,
|
|
970
|
-
method: msg.method,
|
|
971
|
-
error: { code: -32603, message }
|
|
972
|
-
});
|
|
1589
|
+
await this.sendErrorResponse(id, -32603, message);
|
|
973
1590
|
}
|
|
974
1591
|
}
|
|
975
1592
|
};
|
|
976
1593
|
function textContent(text) {
|
|
977
1594
|
return { type: "text", text };
|
|
978
1595
|
}
|
|
1596
|
+
function imageContent(mimeType, data) {
|
|
1597
|
+
return { type: "image", mimeType, data };
|
|
1598
|
+
}
|
|
1599
|
+
function audioContent(mimeType, data) {
|
|
1600
|
+
return { type: "audio", mimeType, data };
|
|
1601
|
+
}
|
|
979
1602
|
function extractText(block) {
|
|
980
1603
|
if (typeof block !== "object" || block === null) return "";
|
|
981
1604
|
const b = block;
|
|
982
1605
|
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
1606
|
+
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
1607
|
+
return b.resource.text;
|
|
1608
|
+
}
|
|
983
1609
|
return "";
|
|
984
1610
|
}
|
|
985
|
-
|
|
986
|
-
|
|
1611
|
+
function isRecord(v) {
|
|
1612
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1613
|
+
}
|
|
1614
|
+
function emptyRunResult(stopReason) {
|
|
1615
|
+
return {
|
|
1616
|
+
text: "",
|
|
1617
|
+
stopReason,
|
|
1618
|
+
hasText: false,
|
|
1619
|
+
toolCalls: [],
|
|
1620
|
+
diffs: [],
|
|
1621
|
+
thoughts: ""
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
987
1624
|
async function makeACPSubagentRunner(options) {
|
|
988
1625
|
const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
|
|
989
1626
|
const wrappedRunner = async (task, ctx) => {
|
|
@@ -998,42 +1635,69 @@ async function makeACPSubagentRunner(options) {
|
|
|
998
1635
|
async function makeACPSubagentRunnerWithStop(options) {
|
|
999
1636
|
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
1000
1637
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
1638
|
+
const persistent = options.persistent === true;
|
|
1639
|
+
let shared = null;
|
|
1640
|
+
const startSession = async () => {
|
|
1641
|
+
return ACPSession.start({
|
|
1642
|
+
command: options.command,
|
|
1643
|
+
...options.args !== void 0 ? { args: options.args } : {},
|
|
1644
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
1645
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1646
|
+
projectRoot,
|
|
1647
|
+
timeoutMs,
|
|
1648
|
+
role: options.role,
|
|
1649
|
+
...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
|
|
1650
|
+
...options.mcpServers !== void 0 ? { mcpServers: options.mcpServers } : {}
|
|
1651
|
+
});
|
|
1652
|
+
};
|
|
1001
1653
|
const runner = async (task, ctx) => {
|
|
1002
|
-
let session
|
|
1654
|
+
let session;
|
|
1655
|
+
const reuse = persistent && shared !== null;
|
|
1003
1656
|
try {
|
|
1004
|
-
session = await
|
|
1005
|
-
|
|
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
|
|
1012
|
-
});
|
|
1657
|
+
session = reuse ? shared : await startSession();
|
|
1658
|
+
if (persistent) shared = session;
|
|
1013
1659
|
} catch (err) {
|
|
1014
1660
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1015
1661
|
}
|
|
1662
|
+
const onProgress = (event) => {
|
|
1663
|
+
try {
|
|
1664
|
+
ctx.budget.markActivity();
|
|
1665
|
+
} catch {
|
|
1666
|
+
}
|
|
1667
|
+
options.onProgress?.(event);
|
|
1668
|
+
};
|
|
1016
1669
|
try {
|
|
1017
|
-
const result = await session.prompt(
|
|
1670
|
+
const result = await session.prompt(
|
|
1671
|
+
[textContent(task.description)],
|
|
1672
|
+
ctx.signal,
|
|
1673
|
+
onProgress
|
|
1674
|
+
);
|
|
1018
1675
|
return {
|
|
1019
1676
|
result: result.text,
|
|
1020
1677
|
iterations: 1,
|
|
1021
|
-
toolCalls:
|
|
1678
|
+
toolCalls: result.toolCalls.length
|
|
1022
1679
|
};
|
|
1023
1680
|
} catch (err) {
|
|
1024
|
-
if (err instanceof ACPSessionError && err.kind === "aborted") {
|
|
1025
|
-
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1026
|
-
}
|
|
1027
1681
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1028
1682
|
} finally {
|
|
1683
|
+
if (!persistent) {
|
|
1684
|
+
try {
|
|
1685
|
+
await session.close();
|
|
1686
|
+
} catch {
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
const stop = async () => {
|
|
1692
|
+
if (shared) {
|
|
1693
|
+
const s = shared;
|
|
1694
|
+
shared = null;
|
|
1029
1695
|
try {
|
|
1030
|
-
await
|
|
1696
|
+
await s.close();
|
|
1031
1697
|
} catch {
|
|
1032
1698
|
}
|
|
1033
1699
|
}
|
|
1034
1700
|
};
|
|
1035
|
-
const stop = () => {
|
|
1036
|
-
};
|
|
1037
1701
|
return { runner, stop };
|
|
1038
1702
|
}
|
|
1039
1703
|
function acpErrorToSubagentError(err, subagentId) {
|
|
@@ -1072,6 +1736,9 @@ function mapACPKind(acpKind) {
|
|
|
1072
1736
|
return "bridge_failed";
|
|
1073
1737
|
case "prompt_failed":
|
|
1074
1738
|
return "tool_failed";
|
|
1739
|
+
case "auth_failed":
|
|
1740
|
+
case "logout_failed":
|
|
1741
|
+
return "bridge_failed";
|
|
1075
1742
|
case "aborted":
|
|
1076
1743
|
return "aborted_by_parent";
|
|
1077
1744
|
case "closed":
|
|
@@ -1092,6 +1759,6 @@ function isRetryable(kind) {
|
|
|
1092
1759
|
}
|
|
1093
1760
|
}
|
|
1094
1761
|
|
|
1095
|
-
export { ClientTransport, ToolTranslator, makeACPSubagentRunner };
|
|
1762
|
+
export { ACPSession, ACPSessionError, ClientTransport, ToolTranslator, WebSocketClientTransport, audioContent, defaultPermissionPolicy, imageContent, makeACPSubagentRunner, makePermissionPolicy, readOnlyPermissionPolicy, textContent };
|
|
1096
1763
|
//# sourceMappingURL=client.js.map
|
|
1097
1764
|
//# sourceMappingURL=client.js.map
|