@wrongstack/acp 0.274.0 → 0.275.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +376 -0
- package/dist/acp-subagent-runner-BAlo23L-.d.ts +644 -0
- package/dist/acp-v1-BxskPsdo.d.ts +520 -0
- package/dist/agent.d.ts +34 -61
- package/dist/agent.js +796 -32
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +3 -2
- package/dist/client.js +779 -112
- package/dist/client.js.map +1 -1
- package/dist/index-DEEYyEpu.d.ts +54 -0
- package/dist/index.d.ts +186 -227
- package/dist/index.js +1881 -286
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +12 -0
- package/dist/sdk.js +3350 -0
- package/dist/sdk.js.map +1 -0
- package/dist/server-agent-turn-C3U0lhA-.d.ts +163 -0
- package/dist/terminal-server-P9KpMZTT.d.ts +99 -0
- package/dist/{tools-registry-BCf8evEG.d.ts → tools-registry-D2xdbzN7.d.ts} +1 -1
- package/dist/wrongstack-acp-agent-nzrqmJnc.d.ts +341 -0
- package/dist/wrongstack-acp-agent.d.ts +2 -2
- package/dist/wrongstack-acp-agent.js +426 -26
- package/dist/wrongstack-acp-agent.js.map +1 -1
- package/package.json +7 -2
- package/dist/index-BvPqJHhm.d.ts +0 -119
- package/dist/stdio-transport-CsFr8JzC.d.ts +0 -205
- package/dist/wrongstack-acp-agent-Dv-A0bEm.d.ts +0 -310
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { writeErr, expectDefined } from '@wrongstack/core';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
+
import { createServer } from 'http';
|
|
4
|
+
import { SubagentBudget } from '@wrongstack/core/coordination';
|
|
3
5
|
import * as fsp from 'fs/promises';
|
|
4
6
|
import * as path from 'path';
|
|
5
7
|
import { spawn } from 'child_process';
|
|
6
|
-
import { SubagentBudget } from '@wrongstack/core/coordination';
|
|
7
8
|
|
|
8
9
|
// src/agent/stdio-transport.ts
|
|
9
10
|
var StdioTransport = class {
|
|
@@ -110,9 +111,10 @@ var ClientTransport = class {
|
|
|
110
111
|
}
|
|
111
112
|
async start() {
|
|
112
113
|
if (this.child) return;
|
|
113
|
-
const [{ spawn: spawn3 }, { buildChildEnv }] = await Promise.all([
|
|
114
|
+
const [{ spawn: spawn3 }, { buildChildEnv }, os] = await Promise.all([
|
|
114
115
|
import('child_process'),
|
|
115
|
-
import('@wrongstack/core')
|
|
116
|
+
import('@wrongstack/core'),
|
|
117
|
+
import('os')
|
|
116
118
|
]);
|
|
117
119
|
return new Promise((resolve3, reject) => {
|
|
118
120
|
const timeout = setTimeout(() => {
|
|
@@ -120,10 +122,12 @@ var ClientTransport = class {
|
|
|
120
122
|
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
121
123
|
);
|
|
122
124
|
}, this.opts.handshakeTimeoutMs);
|
|
125
|
+
const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
|
|
126
|
+
const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
|
|
123
127
|
try {
|
|
124
128
|
this.child = spawn3(this.opts.command, this.opts.args ?? [], {
|
|
125
129
|
env: { ...buildChildEnv(), ...this.opts.env },
|
|
126
|
-
cwd:
|
|
130
|
+
cwd: spawnCwd,
|
|
127
131
|
stdio: ["pipe", "pipe", "pipe"],
|
|
128
132
|
windowsHide: true,
|
|
129
133
|
// On Windows, most ACP-supporting tools (claude, gemini, codex,
|
|
@@ -142,17 +146,39 @@ var ClientTransport = class {
|
|
|
142
146
|
}
|
|
143
147
|
const child = this.child;
|
|
144
148
|
child.stdout.setEncoding("utf8");
|
|
149
|
+
let settled = false;
|
|
150
|
+
const onSpawnFailure = (err) => {
|
|
151
|
+
if (settled) {
|
|
152
|
+
this.closed = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
settled = true;
|
|
156
|
+
clearTimeout(timeout);
|
|
157
|
+
reject(err);
|
|
158
|
+
};
|
|
159
|
+
child.on("error", onSpawnFailure);
|
|
160
|
+
child.stdout.on("error", onSpawnFailure);
|
|
161
|
+
if (this.opts.skipHandshakeMarker) {
|
|
162
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
163
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
164
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
165
|
+
child.once("spawn", () => {
|
|
166
|
+
if (settled) return;
|
|
167
|
+
settled = true;
|
|
168
|
+
clearTimeout(timeout);
|
|
169
|
+
resolve3();
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
145
173
|
const onReady = () => {
|
|
174
|
+
if (settled) return;
|
|
175
|
+
settled = true;
|
|
146
176
|
child.stdout.on("data", (c) => this.onChildData(c));
|
|
147
177
|
child.stderr.on("data", (c) => this.onChildError(c));
|
|
148
178
|
child.on("close", (code) => this.onChildClose(code));
|
|
149
179
|
clearTimeout(timeout);
|
|
150
180
|
resolve3();
|
|
151
181
|
};
|
|
152
|
-
if (this.opts.skipHandshakeMarker) {
|
|
153
|
-
onReady();
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
182
|
const waitForMarker = (chunk) => {
|
|
157
183
|
this.buffer += chunk;
|
|
158
184
|
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
@@ -163,14 +189,6 @@ var ClientTransport = class {
|
|
|
163
189
|
}
|
|
164
190
|
};
|
|
165
191
|
child.stdout.on("data", waitForMarker);
|
|
166
|
-
child.stdout.on("error", (err) => {
|
|
167
|
-
clearTimeout(timeout);
|
|
168
|
-
reject(err);
|
|
169
|
-
});
|
|
170
|
-
child.on("error", (err) => {
|
|
171
|
-
clearTimeout(timeout);
|
|
172
|
-
reject(err);
|
|
173
|
-
});
|
|
174
192
|
});
|
|
175
193
|
}
|
|
176
194
|
send(msg) {
|
|
@@ -365,7 +383,16 @@ var ACP_PROTOCOL_VERSION = 1;
|
|
|
365
383
|
function toWire(msg) {
|
|
366
384
|
return msg;
|
|
367
385
|
}
|
|
368
|
-
var WRONGSTACK_VERSION = "0.
|
|
386
|
+
var WRONGSTACK_VERSION = "0.274.1";
|
|
387
|
+
var WRONGSTACK_AUTH_METHODS = [
|
|
388
|
+
{
|
|
389
|
+
id: "wrongstack-auth",
|
|
390
|
+
name: "Run wstack auth",
|
|
391
|
+
description: "Configure a WrongStack model provider in an interactive terminal.",
|
|
392
|
+
type: "terminal",
|
|
393
|
+
args: ["auth"]
|
|
394
|
+
}
|
|
395
|
+
];
|
|
369
396
|
var DEFAULT_MODE_ID = "code";
|
|
370
397
|
var DEFAULT_MODES = [
|
|
371
398
|
{
|
|
@@ -382,9 +409,17 @@ var ACPProtocolHandler = class {
|
|
|
382
409
|
modes;
|
|
383
410
|
configOptions;
|
|
384
411
|
agentName;
|
|
412
|
+
replayFor;
|
|
413
|
+
seedFor;
|
|
414
|
+
store;
|
|
385
415
|
initialized = false;
|
|
416
|
+
clientCapabilities = {};
|
|
386
417
|
sessions = /* @__PURE__ */ new Map();
|
|
387
418
|
nextId = 1;
|
|
419
|
+
// Outbound request correlation (server → client requests, e.g.
|
|
420
|
+
// session/request_permission). Keyed by our own `srv_N` ids.
|
|
421
|
+
pendingOut = /* @__PURE__ */ new Map();
|
|
422
|
+
nextOutId = 1;
|
|
388
423
|
constructor(opts) {
|
|
389
424
|
this.transport = opts.transport;
|
|
390
425
|
this.defaultCwd = opts.defaultCwd;
|
|
@@ -394,6 +429,43 @@ var ACPProtocolHandler = class {
|
|
|
394
429
|
this.modes = opts.modes ?? DEFAULT_MODES;
|
|
395
430
|
this.configOptions = opts.configOptions ?? [];
|
|
396
431
|
this.agentName = opts.agentName ?? "wrongstack";
|
|
432
|
+
this.replayFor = opts.replayFor;
|
|
433
|
+
this.seedFor = opts.seedFor;
|
|
434
|
+
this.store = opts.store;
|
|
435
|
+
if (typeof this.transport.onMessage === "function") {
|
|
436
|
+
this.transport.onMessage((m) => this.maybeResolvePending(m));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Send a request to the client and await its response. Used for
|
|
441
|
+
* server-initiated calls like `session/request_permission`. Rejects on
|
|
442
|
+
* timeout or transport error so the caller can pick a safe fallback.
|
|
443
|
+
*/
|
|
444
|
+
request(method, params, timeoutMs = 6e4) {
|
|
445
|
+
const id = `srv_${this.nextOutId++}`;
|
|
446
|
+
return new Promise((resolve3, reject) => {
|
|
447
|
+
const timer = setTimeout(() => {
|
|
448
|
+
this.pendingOut.delete(id);
|
|
449
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
450
|
+
}, timeoutMs);
|
|
451
|
+
this.pendingOut.set(id, { resolve: resolve3, reject, timer });
|
|
452
|
+
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
453
|
+
clearTimeout(timer);
|
|
454
|
+
this.pendingOut.delete(id);
|
|
455
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
maybeResolvePending(m) {
|
|
460
|
+
const id = m.id;
|
|
461
|
+
if (typeof id !== "string") return;
|
|
462
|
+
const pending = this.pendingOut.get(id);
|
|
463
|
+
if (!pending) return;
|
|
464
|
+
this.pendingOut.delete(id);
|
|
465
|
+
clearTimeout(pending.timer);
|
|
466
|
+
const err = m.error;
|
|
467
|
+
if (err) pending.reject(new Error(err.message ?? "client request failed"));
|
|
468
|
+
else pending.resolve(m.result);
|
|
397
469
|
}
|
|
398
470
|
/**
|
|
399
471
|
* Process one inbound message. Returns true if this was a terminal
|
|
@@ -420,6 +492,11 @@ var ACPProtocolHandler = class {
|
|
|
420
492
|
session.abort.abort();
|
|
421
493
|
}
|
|
422
494
|
this.sessions.clear();
|
|
495
|
+
for (const [, p] of this.pendingOut) {
|
|
496
|
+
clearTimeout(p.timer);
|
|
497
|
+
p.reject(new Error("protocol handler closed"));
|
|
498
|
+
}
|
|
499
|
+
this.pendingOut.clear();
|
|
423
500
|
}
|
|
424
501
|
// ────────────────────────────────────────────────────────────────────
|
|
425
502
|
// Requests
|
|
@@ -435,10 +512,18 @@ var ACPProtocolHandler = class {
|
|
|
435
512
|
return await this.handleInitialize(id, params);
|
|
436
513
|
case "authenticate":
|
|
437
514
|
return await this.handleAuthenticate(id, params);
|
|
515
|
+
case "logout":
|
|
516
|
+
return await this.handleLogout(id, params);
|
|
438
517
|
case "session/new":
|
|
439
518
|
return await this.handleSessionNew(id, params);
|
|
440
519
|
case "session/load":
|
|
441
520
|
return await this.handleSessionLoad(id, params);
|
|
521
|
+
case "session/resume":
|
|
522
|
+
return await this.handleSessionResume(id, params);
|
|
523
|
+
case "session/close":
|
|
524
|
+
return await this.handleSessionClose(id, params);
|
|
525
|
+
case "session/delete":
|
|
526
|
+
return await this.handleSessionDelete(id, params);
|
|
442
527
|
case "session/prompt":
|
|
443
528
|
return await this.handleSessionPrompt(id, params);
|
|
444
529
|
case "session/set_mode":
|
|
@@ -447,6 +532,16 @@ var ACPProtocolHandler = class {
|
|
|
447
532
|
return await this.handleSetConfigOption(id, params);
|
|
448
533
|
case "session/list":
|
|
449
534
|
return await this.handleSessionList(id);
|
|
535
|
+
case "session/fork":
|
|
536
|
+
return await this.handleSessionFork(id, params);
|
|
537
|
+
case "providers/list":
|
|
538
|
+
return await this.handleProvidersList(id, params);
|
|
539
|
+
case "providers/set":
|
|
540
|
+
return await this.handleProvidersSet(id, params);
|
|
541
|
+
case "providers/disable":
|
|
542
|
+
return await this.handleProvidersDisable(id, params);
|
|
543
|
+
case "mcp/message":
|
|
544
|
+
return await this.handleMcpMessage(id, params);
|
|
450
545
|
default:
|
|
451
546
|
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
452
547
|
return false;
|
|
@@ -459,14 +554,8 @@ var ACPProtocolHandler = class {
|
|
|
459
554
|
}
|
|
460
555
|
async handleInitialize(id, params) {
|
|
461
556
|
const p = params ?? {};
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
await this.sendError(
|
|
465
|
-
id,
|
|
466
|
-
-32e3,
|
|
467
|
-
`server speaks protocolVersion=${ACP_PROTOCOL_VERSION}, client requested ${requested}`
|
|
468
|
-
);
|
|
469
|
-
return false;
|
|
557
|
+
if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
|
|
558
|
+
this.clientCapabilities = p.clientCapabilities;
|
|
470
559
|
}
|
|
471
560
|
this.initialized = true;
|
|
472
561
|
await this.transport.send(toWire({
|
|
@@ -477,9 +566,25 @@ var ACPProtocolHandler = class {
|
|
|
477
566
|
agentCapabilities: {
|
|
478
567
|
loadSession: true,
|
|
479
568
|
promptCapabilities: {
|
|
480
|
-
image
|
|
569
|
+
// We route ACP image blocks into the core agent's multimodal
|
|
570
|
+
// input (server-agent-turn.promptToAgentInput); whether the
|
|
571
|
+
// model can see them is the configured provider's concern.
|
|
572
|
+
image: true,
|
|
481
573
|
audio: false,
|
|
482
574
|
embeddedContext: true
|
|
575
|
+
},
|
|
576
|
+
mcpCapabilities: {
|
|
577
|
+
http: false,
|
|
578
|
+
sse: false
|
|
579
|
+
},
|
|
580
|
+
sessionCapabilities: {
|
|
581
|
+
close: {},
|
|
582
|
+
list: {},
|
|
583
|
+
delete: {},
|
|
584
|
+
resume: {}
|
|
585
|
+
},
|
|
586
|
+
auth: {
|
|
587
|
+
logout: {}
|
|
483
588
|
}
|
|
484
589
|
},
|
|
485
590
|
agentInfo: {
|
|
@@ -487,10 +592,7 @@ var ACPProtocolHandler = class {
|
|
|
487
592
|
title: "WrongStack",
|
|
488
593
|
version: WRONGSTACK_VERSION
|
|
489
594
|
},
|
|
490
|
-
|
|
491
|
-
// re-sent on every `current_mode_update` / `config_option_update`
|
|
492
|
-
// notification so late-joining clients see them.
|
|
493
|
-
authMethods: [],
|
|
595
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
494
596
|
modes: this.modes,
|
|
495
597
|
configOptions: this.configOptions
|
|
496
598
|
}
|
|
@@ -505,6 +607,14 @@ var ACPProtocolHandler = class {
|
|
|
505
607
|
}));
|
|
506
608
|
return false;
|
|
507
609
|
}
|
|
610
|
+
async handleLogout(id, _params) {
|
|
611
|
+
await this.transport.send(toWire({
|
|
612
|
+
jsonrpc: "2.0",
|
|
613
|
+
id,
|
|
614
|
+
result: {}
|
|
615
|
+
}));
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
508
618
|
async handleSessionNew(id, params) {
|
|
509
619
|
const p = params ?? {};
|
|
510
620
|
const cwd = typeof p.cwd === "string" ? p.cwd : this.defaultCwd;
|
|
@@ -520,6 +630,7 @@ var ACPProtocolHandler = class {
|
|
|
520
630
|
};
|
|
521
631
|
this.sessions.set(sessionId, state);
|
|
522
632
|
this.onSessionNew(state);
|
|
633
|
+
await this.persist(state);
|
|
523
634
|
await this.sendNotification({
|
|
524
635
|
sessionId,
|
|
525
636
|
update: {
|
|
@@ -548,7 +659,173 @@ var ACPProtocolHandler = class {
|
|
|
548
659
|
return false;
|
|
549
660
|
}
|
|
550
661
|
async handleSessionLoad(id, params) {
|
|
551
|
-
|
|
662
|
+
const p = params ?? {};
|
|
663
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
664
|
+
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
665
|
+
let existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
666
|
+
if (!existing && sessionId && this.store) {
|
|
667
|
+
const persisted = await this.store.load(sessionId);
|
|
668
|
+
if (persisted) {
|
|
669
|
+
const restored = {
|
|
670
|
+
id: sessionId,
|
|
671
|
+
cwd: persisted.cwd ?? loadCwd ?? this.defaultCwd,
|
|
672
|
+
abort: new AbortController(),
|
|
673
|
+
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
674
|
+
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
675
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
676
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
677
|
+
};
|
|
678
|
+
this.sessions.set(sessionId, restored);
|
|
679
|
+
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
680
|
+
for (const update of persisted.history ?? []) {
|
|
681
|
+
await this.sendNotification({ sessionId, update });
|
|
682
|
+
}
|
|
683
|
+
await this.sendNotification({
|
|
684
|
+
sessionId,
|
|
685
|
+
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
686
|
+
});
|
|
687
|
+
await this.transport.send(toWire({
|
|
688
|
+
jsonrpc: "2.0",
|
|
689
|
+
id,
|
|
690
|
+
result: {
|
|
691
|
+
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
692
|
+
}
|
|
693
|
+
}));
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (existing) {
|
|
698
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
699
|
+
const replay = sessionId ? this.replayFor?.(sessionId) : void 0;
|
|
700
|
+
if (replay) {
|
|
701
|
+
for (const update of replay) {
|
|
702
|
+
await this.sendNotification({ sessionId, update });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
await this.sendNotification({
|
|
706
|
+
sessionId,
|
|
707
|
+
update: {
|
|
708
|
+
sessionUpdate: "session_info_update",
|
|
709
|
+
updatedAt: existing.updatedAt
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
await this.sendNotification({
|
|
713
|
+
sessionId,
|
|
714
|
+
update: {
|
|
715
|
+
sessionUpdate: "current_mode_update",
|
|
716
|
+
modeId: existing.modeId
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
await this.transport.send(toWire({
|
|
720
|
+
jsonrpc: "2.0",
|
|
721
|
+
id,
|
|
722
|
+
result: {
|
|
723
|
+
initialMode: {
|
|
724
|
+
currentModeId: existing.modeId,
|
|
725
|
+
availableModes: this.modes
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}));
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
async handleSessionResume(id, params) {
|
|
735
|
+
const p = params ?? {};
|
|
736
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
737
|
+
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
738
|
+
if (existing) {
|
|
739
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
740
|
+
await this.transport.send(toWire({
|
|
741
|
+
jsonrpc: "2.0",
|
|
742
|
+
id,
|
|
743
|
+
result: {
|
|
744
|
+
initialMode: {
|
|
745
|
+
currentModeId: existing.modeId,
|
|
746
|
+
availableModes: this.modes
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}));
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
async handleSessionClose(id, params) {
|
|
756
|
+
const p = params ?? {};
|
|
757
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
758
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
759
|
+
if (!session) {
|
|
760
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
session.abort.abort();
|
|
764
|
+
if (sessionId) this.sessions.delete(sessionId);
|
|
765
|
+
await this.transport.send(toWire({
|
|
766
|
+
jsonrpc: "2.0",
|
|
767
|
+
id,
|
|
768
|
+
result: {}
|
|
769
|
+
}));
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
async handleSessionDelete(id, params) {
|
|
773
|
+
const p = params ?? {};
|
|
774
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
775
|
+
if (!sessionId) {
|
|
776
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
if (!this.sessions.has(sessionId)) {
|
|
780
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
const session = this.sessions.get(sessionId);
|
|
784
|
+
session.abort.abort();
|
|
785
|
+
this.sessions.delete(sessionId);
|
|
786
|
+
await this.transport.send(toWire({
|
|
787
|
+
jsonrpc: "2.0",
|
|
788
|
+
id,
|
|
789
|
+
result: {}
|
|
790
|
+
}));
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
async handleSessionFork(id, params) {
|
|
794
|
+
const p = params ?? {};
|
|
795
|
+
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
796
|
+
if (!sourceId || !this.sessions.has(sourceId)) {
|
|
797
|
+
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
const forkParams = params;
|
|
801
|
+
return this.handleSessionNew(id, { ...forkParams, cwd: p.cwd ?? this.defaultCwd });
|
|
802
|
+
}
|
|
803
|
+
async handleProvidersList(id, _params) {
|
|
804
|
+
await this.transport.send(toWire({
|
|
805
|
+
jsonrpc: "2.0",
|
|
806
|
+
id,
|
|
807
|
+
result: {
|
|
808
|
+
providers: [],
|
|
809
|
+
currentProviderId: null
|
|
810
|
+
}
|
|
811
|
+
}));
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
async handleProvidersSet(id, _params) {
|
|
815
|
+
await this.sendError(id, -32e3, "provider configuration not available through ACP; use wstack auth");
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
async handleProvidersDisable(id, _params) {
|
|
819
|
+
await this.transport.send(toWire({
|
|
820
|
+
jsonrpc: "2.0",
|
|
821
|
+
id,
|
|
822
|
+
result: {}
|
|
823
|
+
}));
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
async handleMcpMessage(id, _params) {
|
|
827
|
+
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
828
|
+
return false;
|
|
552
829
|
}
|
|
553
830
|
async handleSessionPrompt(id, params) {
|
|
554
831
|
const p = params ?? {};
|
|
@@ -568,11 +845,54 @@ var ACPProtocolHandler = class {
|
|
|
568
845
|
const turnSignal = new AbortController();
|
|
569
846
|
const onCancel = () => turnSignal.abort();
|
|
570
847
|
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
848
|
+
const api = {
|
|
849
|
+
clientCapabilities: this.clientCapabilities,
|
|
850
|
+
requestPermission: async (req) => {
|
|
851
|
+
const res = await this.request("session/request_permission", {
|
|
852
|
+
sessionId,
|
|
853
|
+
toolCall: req.toolCall,
|
|
854
|
+
options: req.options
|
|
855
|
+
});
|
|
856
|
+
const outcome = res?.outcome;
|
|
857
|
+
return outcome ?? { outcome: "cancelled" };
|
|
858
|
+
},
|
|
859
|
+
readTextFile: async (params2) => {
|
|
860
|
+
const res = await this.request("fs/read_text_file", { sessionId, ...params2 });
|
|
861
|
+
return String(res?.content ?? "");
|
|
862
|
+
},
|
|
863
|
+
writeTextFile: async (params2) => {
|
|
864
|
+
await this.request("fs/write_text_file", { sessionId, ...params2 });
|
|
865
|
+
},
|
|
866
|
+
runTerminal: async ({ command, args, cwd }) => {
|
|
867
|
+
const created = await this.request("terminal/create", {
|
|
868
|
+
sessionId,
|
|
869
|
+
command,
|
|
870
|
+
...args ? { args } : {},
|
|
871
|
+
...cwd ? { cwd } : {}
|
|
872
|
+
});
|
|
873
|
+
const terminalId = created?.terminalId;
|
|
874
|
+
if (!terminalId) return { output: "", exitCode: null };
|
|
875
|
+
try {
|
|
876
|
+
const exit = await this.request("terminal/wait_for_exit", { sessionId, terminalId });
|
|
877
|
+
const out = await this.request("terminal/output", { sessionId, terminalId });
|
|
878
|
+
return {
|
|
879
|
+
output: String(out?.output ?? ""),
|
|
880
|
+
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
|
|
881
|
+
};
|
|
882
|
+
} finally {
|
|
883
|
+
try {
|
|
884
|
+
await this.request("terminal/release", { sessionId, terminalId });
|
|
885
|
+
} catch {
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
};
|
|
571
890
|
let result;
|
|
572
891
|
try {
|
|
573
892
|
result = await this.runTurn(
|
|
574
893
|
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
575
|
-
(update) => this.sendNotification({ sessionId, update })
|
|
894
|
+
(update) => this.sendNotification({ sessionId, update }),
|
|
895
|
+
api
|
|
576
896
|
);
|
|
577
897
|
} catch (err) {
|
|
578
898
|
session.abort.signal.removeEventListener("abort", onCancel);
|
|
@@ -582,6 +902,7 @@ var ACPProtocolHandler = class {
|
|
|
582
902
|
}
|
|
583
903
|
session.abort.signal.removeEventListener("abort", onCancel);
|
|
584
904
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
905
|
+
await this.persist(session);
|
|
585
906
|
await this.transport.send(toWire({
|
|
586
907
|
jsonrpc: "2.0",
|
|
587
908
|
id,
|
|
@@ -610,12 +931,12 @@ var ACPProtocolHandler = class {
|
|
|
610
931
|
async handleSetConfigOption(id, params) {
|
|
611
932
|
const p = params ?? {};
|
|
612
933
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
613
|
-
const optionId = typeof p.
|
|
934
|
+
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
614
935
|
const value = typeof p.value === "string" ? p.value : null;
|
|
615
936
|
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
616
937
|
const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
|
|
617
938
|
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
618
|
-
await this.sendError(id, -32602, "invalid sessionId,
|
|
939
|
+
await this.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
619
940
|
return false;
|
|
620
941
|
}
|
|
621
942
|
option.currentValue = value;
|
|
@@ -627,7 +948,7 @@ var ACPProtocolHandler = class {
|
|
|
627
948
|
configOptions: [...this.configOptions]
|
|
628
949
|
}
|
|
629
950
|
});
|
|
630
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
|
|
951
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
631
952
|
return false;
|
|
632
953
|
}
|
|
633
954
|
async handleSessionList(id) {
|
|
@@ -661,6 +982,9 @@ var ACPProtocolHandler = class {
|
|
|
661
982
|
}
|
|
662
983
|
return false;
|
|
663
984
|
}
|
|
985
|
+
case "$/cancel_request": {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
664
988
|
case "exit":
|
|
665
989
|
this.close();
|
|
666
990
|
return true;
|
|
@@ -674,6 +998,14 @@ var ACPProtocolHandler = class {
|
|
|
674
998
|
async sendNotification(params) {
|
|
675
999
|
await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
|
|
676
1000
|
}
|
|
1001
|
+
/** Best-effort durable persistence of a session + its recorded history. */
|
|
1002
|
+
async persist(state) {
|
|
1003
|
+
if (!this.store) return;
|
|
1004
|
+
try {
|
|
1005
|
+
await this.store.save(state, this.replayFor?.(state.id));
|
|
1006
|
+
} catch {
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
677
1009
|
async sendError(id, code, message, data) {
|
|
678
1010
|
const error = { code, message };
|
|
679
1011
|
if (data !== void 0) error.data = data;
|
|
@@ -701,26 +1033,41 @@ function errorToJsonRpc(err) {
|
|
|
701
1033
|
var WrongStackACPServer = class {
|
|
702
1034
|
transport;
|
|
703
1035
|
handler;
|
|
1036
|
+
options;
|
|
1037
|
+
/** HTTP server when transport mode is HTTP. */
|
|
1038
|
+
httpServer = null;
|
|
704
1039
|
running = false;
|
|
705
1040
|
constructor(opts = {}) {
|
|
1041
|
+
this.options = opts;
|
|
706
1042
|
this.transport = new StdioTransport();
|
|
707
1043
|
const runTurn = opts.runTurn ?? defaultEchoRunTurn;
|
|
708
1044
|
this.handler = new ACPProtocolHandler({
|
|
709
1045
|
transport: this.transport,
|
|
710
1046
|
defaultCwd: opts.defaultCwd ?? process.cwd(),
|
|
711
1047
|
runTurn,
|
|
712
|
-
agentName: opts.agentName
|
|
1048
|
+
agentName: opts.agentName,
|
|
1049
|
+
...opts.replayFor ? { replayFor: opts.replayFor } : {},
|
|
1050
|
+
...opts.seedFor ? { seedFor: opts.seedFor } : {},
|
|
1051
|
+
...opts.store ? { store: opts.store } : {}
|
|
713
1052
|
});
|
|
714
1053
|
}
|
|
715
1054
|
/**
|
|
716
|
-
* Start the server.
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
* process is the ACP server (the old `StdioTransport` handshake).
|
|
720
|
-
* 2. Loop: read messages, dispatch to the handler, until EOF / error.
|
|
1055
|
+
* Start the server. Mode depends on `options.transport`:
|
|
1056
|
+
* - 'stdio' (default): reads JSON-RPC from stdin, writes to stdout.
|
|
1057
|
+
* - number: listens as HTTP on the given port.
|
|
721
1058
|
*/
|
|
722
1059
|
async start() {
|
|
723
|
-
this.transport
|
|
1060
|
+
const transportMode = this.options.transport;
|
|
1061
|
+
if (typeof transportMode === "number") {
|
|
1062
|
+
await this.startHttp(transportMode);
|
|
1063
|
+
} else {
|
|
1064
|
+
await this.startStdio();
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
async startStdio() {
|
|
1068
|
+
if (this.options.legacyStartupMarker) {
|
|
1069
|
+
this.transport.sendStartupMarker();
|
|
1070
|
+
}
|
|
724
1071
|
this.running = true;
|
|
725
1072
|
while (this.running) {
|
|
726
1073
|
const msg = await this.transport.read();
|
|
@@ -730,10 +1077,80 @@ var WrongStackACPServer = class {
|
|
|
730
1077
|
}
|
|
731
1078
|
this.transport.close();
|
|
732
1079
|
}
|
|
1080
|
+
async startHttp(port) {
|
|
1081
|
+
const host = this.options.host ?? "127.0.0.1";
|
|
1082
|
+
const handler = this.handler;
|
|
1083
|
+
this.httpServer = createServer(async (req, res) => {
|
|
1084
|
+
const selfOrigin = `http://${host}:${port}`;
|
|
1085
|
+
const reqOrigin = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin;
|
|
1086
|
+
if (reqOrigin && reqOrigin !== selfOrigin) {
|
|
1087
|
+
res.writeHead(403);
|
|
1088
|
+
res.end(JSON.stringify({ error: "cross-origin request forbidden" }));
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (reqOrigin) res.setHeader("Access-Control-Allow-Origin", reqOrigin);
|
|
1092
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
1093
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
|
1094
|
+
if (req.method === "OPTIONS") {
|
|
1095
|
+
res.writeHead(204);
|
|
1096
|
+
res.end();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (req.method !== "POST") {
|
|
1100
|
+
res.writeHead(405);
|
|
1101
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
let body = "";
|
|
1105
|
+
for await (const chunk of req) {
|
|
1106
|
+
body += chunk;
|
|
1107
|
+
}
|
|
1108
|
+
let msg;
|
|
1109
|
+
try {
|
|
1110
|
+
msg = JSON.parse(body);
|
|
1111
|
+
} catch {
|
|
1112
|
+
res.writeHead(400);
|
|
1113
|
+
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const notifications = [];
|
|
1117
|
+
let response = null;
|
|
1118
|
+
const originalSend = this.transport.send.bind(this.transport);
|
|
1119
|
+
this.transport.send = async (m) => {
|
|
1120
|
+
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
1121
|
+
response = m;
|
|
1122
|
+
} else if (m.method === "session/update") {
|
|
1123
|
+
notifications.push(m.params);
|
|
1124
|
+
} else {
|
|
1125
|
+
notifications.push(m);
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
try {
|
|
1129
|
+
await handler.handleMessage(msg);
|
|
1130
|
+
} finally {
|
|
1131
|
+
this.transport.send = originalSend;
|
|
1132
|
+
}
|
|
1133
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1134
|
+
const responseBody = response !== null ? { ...response, notifications } : { notifications };
|
|
1135
|
+
res.end(JSON.stringify(responseBody));
|
|
1136
|
+
});
|
|
1137
|
+
return new Promise((resolve3) => {
|
|
1138
|
+
this.httpServer.listen(port, host, () => {
|
|
1139
|
+
writeErr(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1140
|
+
`);
|
|
1141
|
+
this.running = true;
|
|
1142
|
+
resolve3();
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
733
1146
|
/** Stop the server. */
|
|
734
1147
|
stop() {
|
|
735
1148
|
this.running = false;
|
|
736
1149
|
this.transport.close();
|
|
1150
|
+
if (this.httpServer) {
|
|
1151
|
+
this.httpServer.close();
|
|
1152
|
+
this.httpServer = null;
|
|
1153
|
+
}
|
|
737
1154
|
}
|
|
738
1155
|
};
|
|
739
1156
|
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
@@ -751,6 +1168,115 @@ if (isEntrypoint) {
|
|
|
751
1168
|
process.exit(1);
|
|
752
1169
|
});
|
|
753
1170
|
}
|
|
1171
|
+
|
|
1172
|
+
// src/client/websocket-transport.ts
|
|
1173
|
+
var WebSocketClientTransport = class {
|
|
1174
|
+
ws = null;
|
|
1175
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1176
|
+
closed = false;
|
|
1177
|
+
opts;
|
|
1178
|
+
constructor(opts) {
|
|
1179
|
+
this.opts = opts;
|
|
1180
|
+
}
|
|
1181
|
+
start() {
|
|
1182
|
+
const WS = globalThis.WebSocket;
|
|
1183
|
+
if (!WS) {
|
|
1184
|
+
return Promise.reject(
|
|
1185
|
+
new Error(
|
|
1186
|
+
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
1187
|
+
)
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
1191
|
+
return new Promise((resolve3, reject) => {
|
|
1192
|
+
let settled = false;
|
|
1193
|
+
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
1194
|
+
this.ws = ws;
|
|
1195
|
+
const timer = setTimeout(() => {
|
|
1196
|
+
if (settled) return;
|
|
1197
|
+
settled = true;
|
|
1198
|
+
try {
|
|
1199
|
+
ws.close();
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
1203
|
+
}, timeoutMs);
|
|
1204
|
+
ws.addEventListener("open", () => {
|
|
1205
|
+
if (settled) return;
|
|
1206
|
+
settled = true;
|
|
1207
|
+
clearTimeout(timer);
|
|
1208
|
+
resolve3();
|
|
1209
|
+
});
|
|
1210
|
+
ws.addEventListener("error", (ev) => {
|
|
1211
|
+
if (settled) {
|
|
1212
|
+
this.closed = true;
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
settled = true;
|
|
1216
|
+
clearTimeout(timer);
|
|
1217
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
1218
|
+
reject(new Error(message));
|
|
1219
|
+
});
|
|
1220
|
+
ws.addEventListener("close", () => {
|
|
1221
|
+
this.closed = true;
|
|
1222
|
+
});
|
|
1223
|
+
ws.addEventListener("message", (ev) => {
|
|
1224
|
+
this.onData(ev.data);
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
send(msg) {
|
|
1229
|
+
if (this.closed || !this.ws) {
|
|
1230
|
+
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
1231
|
+
}
|
|
1232
|
+
try {
|
|
1233
|
+
this.ws.send(JSON.stringify(msg));
|
|
1234
|
+
return Promise.resolve();
|
|
1235
|
+
} catch (err) {
|
|
1236
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
onMessage(handler) {
|
|
1240
|
+
this.handlers.add(handler);
|
|
1241
|
+
return () => this.handlers.delete(handler);
|
|
1242
|
+
}
|
|
1243
|
+
stop() {
|
|
1244
|
+
this.closed = true;
|
|
1245
|
+
if (this.ws) {
|
|
1246
|
+
try {
|
|
1247
|
+
this.ws.close();
|
|
1248
|
+
} catch {
|
|
1249
|
+
}
|
|
1250
|
+
this.ws = null;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
onData(data) {
|
|
1254
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
1255
|
+
if (!text.trim()) return;
|
|
1256
|
+
let msg;
|
|
1257
|
+
try {
|
|
1258
|
+
msg = JSON.parse(text);
|
|
1259
|
+
} catch {
|
|
1260
|
+
for (const line of text.split("\n")) {
|
|
1261
|
+
if (!line.trim()) continue;
|
|
1262
|
+
try {
|
|
1263
|
+
this.dispatch(JSON.parse(line));
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
this.dispatch(msg);
|
|
1270
|
+
}
|
|
1271
|
+
dispatch(msg) {
|
|
1272
|
+
for (const handler of [...this.handlers]) {
|
|
1273
|
+
try {
|
|
1274
|
+
handler(msg);
|
|
1275
|
+
} catch {
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
754
1280
|
var DEFAULT_OPTIONS = {
|
|
755
1281
|
asyncTools: true,
|
|
756
1282
|
pollIntervalMs: 500,
|
|
@@ -793,6 +1319,7 @@ var ToolTranslator = class {
|
|
|
793
1319
|
*/
|
|
794
1320
|
async callTool(transport, name, args, callId = crypto.randomUUID()) {
|
|
795
1321
|
await transport.send({
|
|
1322
|
+
jsonrpc: "2.0",
|
|
796
1323
|
method: "tools/call",
|
|
797
1324
|
id: callId,
|
|
798
1325
|
params: { name, arguments: args }
|
|
@@ -815,11 +1342,11 @@ var ToolTranslator = class {
|
|
|
815
1342
|
var FsError = class extends Error {
|
|
816
1343
|
code;
|
|
817
1344
|
path;
|
|
818
|
-
constructor(code,
|
|
1345
|
+
constructor(code, path4, message) {
|
|
819
1346
|
super(message);
|
|
820
1347
|
this.name = "FsError";
|
|
821
1348
|
this.code = code;
|
|
822
|
-
this.path =
|
|
1349
|
+
this.path = path4;
|
|
823
1350
|
}
|
|
824
1351
|
};
|
|
825
1352
|
var FileServer = class {
|
|
@@ -913,12 +1440,11 @@ function randomHex(bytes) {
|
|
|
913
1440
|
}
|
|
914
1441
|
|
|
915
1442
|
// src/client/permission.ts
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const ranked = [...req.options].sort((a, b) => {
|
|
1443
|
+
function pickAllow(options) {
|
|
1444
|
+
const ranked = [...options].sort((a, b) => {
|
|
919
1445
|
const score = (k) => {
|
|
920
|
-
if (k === "
|
|
921
|
-
if (k === "
|
|
1446
|
+
if (k === "allow_once") return 0;
|
|
1447
|
+
if (k === "allow_always") return 1;
|
|
922
1448
|
if (k === "reject_once") return 2;
|
|
923
1449
|
return 3;
|
|
924
1450
|
};
|
|
@@ -929,7 +1455,33 @@ var defaultPermissionPolicy = async (req) => {
|
|
|
929
1455
|
return { outcome: "cancelled" };
|
|
930
1456
|
}
|
|
931
1457
|
return { outcome: "selected", optionId: chosen.optionId };
|
|
1458
|
+
}
|
|
1459
|
+
function pickReject(options) {
|
|
1460
|
+
const reject = options.find(
|
|
1461
|
+
(o) => o.kind === "reject_once" || o.kind === "reject_always"
|
|
1462
|
+
);
|
|
1463
|
+
return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
|
|
1464
|
+
}
|
|
1465
|
+
var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
|
|
1466
|
+
var defaultPermissionPolicy = async (req) => {
|
|
1467
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1468
|
+
return pickAllow(req.options);
|
|
1469
|
+
};
|
|
1470
|
+
var readOnlyPermissionPolicy = async (req) => {
|
|
1471
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1472
|
+
const kind = req.toolCall.kind;
|
|
1473
|
+
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
1474
|
+
return pickAllow(req.options);
|
|
1475
|
+
}
|
|
1476
|
+
return pickReject(req.options);
|
|
932
1477
|
};
|
|
1478
|
+
function makePermissionPolicy(decide) {
|
|
1479
|
+
return async (req) => {
|
|
1480
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1481
|
+
const allow = await decide(req);
|
|
1482
|
+
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
933
1485
|
var TerminalServer = class {
|
|
934
1486
|
terminals = /* @__PURE__ */ new Map();
|
|
935
1487
|
projectRoot;
|
|
@@ -1125,6 +1677,12 @@ var ACPSession = class _ACPSession {
|
|
|
1125
1677
|
nextId = 1;
|
|
1126
1678
|
/** True after close() has been called. */
|
|
1127
1679
|
closed = false;
|
|
1680
|
+
// Agent-provided info from the initialize handshake
|
|
1681
|
+
agentCapabilities = {};
|
|
1682
|
+
agentInfo = null;
|
|
1683
|
+
authMethods = [];
|
|
1684
|
+
/** Protocol version negotiated with the agent during initialize. */
|
|
1685
|
+
negotiatedVersion = ACP_PROTOCOL_VERSION;
|
|
1128
1686
|
constructor(opts, transport) {
|
|
1129
1687
|
this.opts = opts;
|
|
1130
1688
|
this.transport = transport;
|
|
@@ -1146,6 +1704,36 @@ var ACPSession = class _ACPSession {
|
|
|
1146
1704
|
this.terminalServer = new TerminalServer(termOpts);
|
|
1147
1705
|
this.permissionPolicy = opts.permissionPolicy ?? defaultPermissionPolicy;
|
|
1148
1706
|
}
|
|
1707
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1708
|
+
// Public accessors
|
|
1709
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1710
|
+
/** Agent capabilities advertised during initialize. */
|
|
1711
|
+
getCapabilities() {
|
|
1712
|
+
return { ...this.agentCapabilities };
|
|
1713
|
+
}
|
|
1714
|
+
/** Authentication methods advertised by the agent. */
|
|
1715
|
+
getAuthMethods() {
|
|
1716
|
+
return [...this.authMethods];
|
|
1717
|
+
}
|
|
1718
|
+
/** Agent info (name, title, version) from initialize. */
|
|
1719
|
+
getAgentInfo() {
|
|
1720
|
+
return this.agentInfo;
|
|
1721
|
+
}
|
|
1722
|
+
/** Whether the agent requires authentication (has auth methods). */
|
|
1723
|
+
requiresAuth() {
|
|
1724
|
+
return this.authMethods.length > 0;
|
|
1725
|
+
}
|
|
1726
|
+
/** Current session id, if one exists. */
|
|
1727
|
+
getSessionId() {
|
|
1728
|
+
return this.sessionId;
|
|
1729
|
+
}
|
|
1730
|
+
/** Protocol version negotiated during initialize. */
|
|
1731
|
+
getNegotiatedVersion() {
|
|
1732
|
+
return this.negotiatedVersion;
|
|
1733
|
+
}
|
|
1734
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1735
|
+
// Lifecycle — start
|
|
1736
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1149
1737
|
/**
|
|
1150
1738
|
* Spawn the child, run the initialize handshake, install the
|
|
1151
1739
|
* message dispatch, and return a ready session.
|
|
@@ -1155,20 +1743,37 @@ var ACPSession = class _ACPSession {
|
|
|
1155
1743
|
command: opts.command,
|
|
1156
1744
|
args: opts.args ? [...opts.args] : [],
|
|
1157
1745
|
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
1746
|
skipHandshakeMarker: true
|
|
1163
1747
|
};
|
|
1164
1748
|
if (opts.env !== void 0) transportOpts.env = opts.env;
|
|
1165
1749
|
if (opts.cwd !== void 0) transportOpts.cwd = opts.cwd;
|
|
1166
1750
|
const transport = new ClientTransport(transportOpts);
|
|
1751
|
+
return _ACPSession.attach(opts, transport, `failed to spawn ${opts.command}`);
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Connect to a REMOTE ACP agent over a WebSocket instead of spawning a
|
|
1755
|
+
* local subprocess. `opts.command` is ignored for the wire (a label is
|
|
1756
|
+
* still useful for `role`); everything else (projectRoot sandbox for
|
|
1757
|
+
* fs/terminal, timeouts, permission policy, MCP servers) applies the same.
|
|
1758
|
+
*/
|
|
1759
|
+
static async connectWebSocket(wsOpts, opts) {
|
|
1760
|
+
const transport = new WebSocketClientTransport(wsOpts);
|
|
1761
|
+
return _ACPSession.attach(opts, transport, `failed to connect to ${wsOpts.url}`);
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Connect using a caller-supplied transport. Lets advanced callers plug
|
|
1765
|
+
* in their own wire (SDK streams, in-process pipes, test doubles).
|
|
1766
|
+
*/
|
|
1767
|
+
static async connect(transport, opts) {
|
|
1768
|
+
return _ACPSession.attach(opts, transport, "failed to connect transport");
|
|
1769
|
+
}
|
|
1770
|
+
/** Shared connect path: start the transport, install dispatch, handshake. */
|
|
1771
|
+
static async attach(opts, transport, spawnErrLabel) {
|
|
1167
1772
|
try {
|
|
1168
1773
|
await transport.start();
|
|
1169
1774
|
} catch (err) {
|
|
1170
1775
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1171
|
-
throw new ACPSessionError("spawn_failed",
|
|
1776
|
+
throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
|
|
1172
1777
|
}
|
|
1173
1778
|
const session = new _ACPSession(opts, transport);
|
|
1174
1779
|
transport.onMessage((msg) => session.handleMessage(msg));
|
|
@@ -1183,14 +1788,16 @@ var ACPSession = class _ACPSession {
|
|
|
1183
1788
|
}
|
|
1184
1789
|
return session;
|
|
1185
1790
|
}
|
|
1791
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1792
|
+
// Initialization
|
|
1793
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1186
1794
|
async initialize() {
|
|
1187
1795
|
const id = this.allocId();
|
|
1188
1796
|
const result = await this.sendRequest(id, "initialize", {
|
|
1189
1797
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
1190
1798
|
clientCapabilities: {
|
|
1191
1799
|
fs: { readTextFile: true, writeTextFile: true },
|
|
1192
|
-
terminal: true
|
|
1193
|
-
promptCapabilities: { image: false, audio: false, embeddedContext: true }
|
|
1800
|
+
terminal: true
|
|
1194
1801
|
},
|
|
1195
1802
|
clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.263.0" }
|
|
1196
1803
|
});
|
|
@@ -1201,53 +1808,344 @@ var ACPSession = class _ACPSession {
|
|
|
1201
1808
|
throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
|
|
1202
1809
|
}
|
|
1203
1810
|
const r = result;
|
|
1204
|
-
if (r.protocolVersion
|
|
1811
|
+
if (r.protocolVersion > ACP_PROTOCOL_VERSION) {
|
|
1205
1812
|
throw new ACPSessionError(
|
|
1206
1813
|
"unsupported_capability",
|
|
1207
|
-
`agent
|
|
1814
|
+
`agent requires protocolVersion=${r.protocolVersion}, client supports up to ${ACP_PROTOCOL_VERSION}`
|
|
1208
1815
|
);
|
|
1209
1816
|
}
|
|
1817
|
+
this.negotiatedVersion = r.protocolVersion;
|
|
1818
|
+
this.agentCapabilities = r.agentCapabilities ?? {};
|
|
1819
|
+
this.agentInfo = r.agentInfo ?? null;
|
|
1820
|
+
this.authMethods = r.authMethods ?? [];
|
|
1210
1821
|
this.state = "ready";
|
|
1211
1822
|
}
|
|
1823
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1824
|
+
// Authentication
|
|
1825
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1212
1826
|
/**
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
* the agent's response.
|
|
1827
|
+
* Authenticate with the agent using one of the advertised auth methods.
|
|
1828
|
+
* Call this AFTER start() and BEFORE any session/new call.
|
|
1216
1829
|
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1830
|
+
* Throws ACPSessionError('auth_failed') if the agent rejects the
|
|
1831
|
+
* authentication or if the methodId is not in the advertised list.
|
|
1832
|
+
*/
|
|
1833
|
+
async authenticate(methodId) {
|
|
1834
|
+
if (this.state === "closed") {
|
|
1835
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
1836
|
+
}
|
|
1837
|
+
if (this.state !== "ready") {
|
|
1838
|
+
throw new ACPSessionError(
|
|
1839
|
+
"protocol_error",
|
|
1840
|
+
`authenticate called in state=${this.state} (expected 'ready')`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
1844
|
+
throw new ACPSessionError(
|
|
1845
|
+
"auth_failed",
|
|
1846
|
+
`auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
const id = this.allocId();
|
|
1850
|
+
const result = await this.sendRequest(id, "authenticate", { methodId });
|
|
1851
|
+
if (isJsonRpcError(result)) {
|
|
1852
|
+
throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
|
|
1853
|
+
}
|
|
1854
|
+
this.state = "authenticated";
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Log out from the current authenticated session.
|
|
1858
|
+
* Only callable if the agent advertises `auth.logout` capability.
|
|
1859
|
+
*/
|
|
1860
|
+
async logout() {
|
|
1861
|
+
if (this.state === "closed") {
|
|
1862
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
1863
|
+
}
|
|
1864
|
+
if (!this.agentCapabilities.auth?.logout) {
|
|
1865
|
+
throw new ACPSessionError(
|
|
1866
|
+
"unsupported_capability",
|
|
1867
|
+
"agent does not support logout (auth.logout capability not advertised)"
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
const id = this.allocId();
|
|
1871
|
+
const result = await this.sendRequest(id, "logout", {});
|
|
1872
|
+
if (isJsonRpcError(result)) {
|
|
1873
|
+
throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
|
|
1874
|
+
}
|
|
1875
|
+
this.state = "ready";
|
|
1876
|
+
}
|
|
1877
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1878
|
+
// Session management
|
|
1879
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1880
|
+
/**
|
|
1881
|
+
* Load an existing session. The agent replays the conversation history
|
|
1882
|
+
* via session/update notifications before responding.
|
|
1883
|
+
*
|
|
1884
|
+
* Only works if the agent advertises `loadSession` capability.
|
|
1885
|
+
*
|
|
1886
|
+
* @param sessionId - The session to load
|
|
1887
|
+
* @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
|
|
1888
|
+
* @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
|
|
1889
|
+
*/
|
|
1890
|
+
async loadSession(sessionId, mcpServers, cwd) {
|
|
1891
|
+
if (this.closed) {
|
|
1892
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
1893
|
+
}
|
|
1894
|
+
if (!this.agentCapabilities.loadSession) {
|
|
1895
|
+
throw new ACPSessionError(
|
|
1896
|
+
"unsupported_capability",
|
|
1897
|
+
"agent does not support session/load (loadSession capability not advertised)"
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
if (this.sessionId) {
|
|
1901
|
+
await this.closeSession();
|
|
1902
|
+
}
|
|
1903
|
+
this.resetScratch();
|
|
1904
|
+
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
1905
|
+
const id = this.allocId();
|
|
1906
|
+
const result = await this.sendRequest(id, "session/load", {
|
|
1907
|
+
sessionId,
|
|
1908
|
+
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
1909
|
+
mcpServers: servers
|
|
1910
|
+
});
|
|
1911
|
+
if (isJsonRpcError(result)) {
|
|
1912
|
+
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
1913
|
+
}
|
|
1914
|
+
this.sessionId = sessionId;
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Resume an existing session without replaying history.
|
|
1918
|
+
*
|
|
1919
|
+
* Only works if the agent advertises `sessionCapabilities.resume`.
|
|
1920
|
+
*
|
|
1921
|
+
* @param sessionId - The session to resume
|
|
1922
|
+
* @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
|
|
1923
|
+
* @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
|
|
1924
|
+
*/
|
|
1925
|
+
async resumeSession(sessionId, mcpServers, cwd) {
|
|
1926
|
+
if (this.closed) {
|
|
1927
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
1928
|
+
}
|
|
1929
|
+
if (!this.agentCapabilities.sessionCapabilities?.resume) {
|
|
1930
|
+
throw new ACPSessionError(
|
|
1931
|
+
"unsupported_capability",
|
|
1932
|
+
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
if (this.sessionId) {
|
|
1936
|
+
await this.closeSession();
|
|
1937
|
+
}
|
|
1938
|
+
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
1939
|
+
const id = this.allocId();
|
|
1940
|
+
const result = await this.sendRequest(id, "session/resume", {
|
|
1941
|
+
sessionId,
|
|
1942
|
+
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
1943
|
+
mcpServers: servers
|
|
1944
|
+
});
|
|
1945
|
+
if (isJsonRpcError(result)) {
|
|
1946
|
+
throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
|
|
1947
|
+
}
|
|
1948
|
+
this.sessionId = sessionId;
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* List existing sessions known to the agent.
|
|
1952
|
+
*
|
|
1953
|
+
* Only works if the agent advertises `sessionCapabilities.list`.
|
|
1954
|
+
*/
|
|
1955
|
+
async listSessions(cursor, cwd) {
|
|
1956
|
+
if (this.closed) {
|
|
1957
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
1958
|
+
}
|
|
1959
|
+
if (!this.agentCapabilities.sessionCapabilities?.list) {
|
|
1960
|
+
throw new ACPSessionError(
|
|
1961
|
+
"unsupported_capability",
|
|
1962
|
+
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
const id = this.allocId();
|
|
1966
|
+
const params = {};
|
|
1967
|
+
if (cursor !== void 0) params.cursor = cursor;
|
|
1968
|
+
if (cwd !== void 0) params.cwd = cwd;
|
|
1969
|
+
const result = await this.sendRequest(id, "session/list", params);
|
|
1970
|
+
if (isJsonRpcError(result)) {
|
|
1971
|
+
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
1972
|
+
}
|
|
1973
|
+
const r = result;
|
|
1974
|
+
return {
|
|
1975
|
+
sessions: r.sessions ?? [],
|
|
1976
|
+
nextCursor: r.nextCursor
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Delete a session from the agent's session list.
|
|
1981
|
+
*
|
|
1982
|
+
* Only works if the agent advertises `sessionCapabilities.delete`.
|
|
1983
|
+
*/
|
|
1984
|
+
async deleteSession(sessionId) {
|
|
1985
|
+
if (this.closed) {
|
|
1986
|
+
throw new ACPSessionError("closed", "session is closed");
|
|
1987
|
+
}
|
|
1988
|
+
if (!this.agentCapabilities.sessionCapabilities?.delete) {
|
|
1989
|
+
throw new ACPSessionError(
|
|
1990
|
+
"unsupported_capability",
|
|
1991
|
+
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
const id = this.allocId();
|
|
1995
|
+
const result = await this.sendRequest(id, "session/delete", { sessionId });
|
|
1996
|
+
if (isJsonRpcError(result)) {
|
|
1997
|
+
throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
|
|
1998
|
+
}
|
|
1999
|
+
if (this.sessionId === sessionId) {
|
|
2000
|
+
this.sessionId = null;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Fork a session — create a new session from an existing one.
|
|
2005
|
+
*/
|
|
2006
|
+
async forkSession(sourceSessionId, cwd, mcpServers) {
|
|
2007
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2008
|
+
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
2009
|
+
const id = this.allocId();
|
|
2010
|
+
const result = await this.sendRequest(id, "session/fork", {
|
|
2011
|
+
sessionId: sourceSessionId,
|
|
2012
|
+
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
2013
|
+
...servers.length > 0 ? { mcpServers: servers } : {}
|
|
2014
|
+
});
|
|
2015
|
+
if (isJsonRpcError(result)) {
|
|
2016
|
+
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
2017
|
+
}
|
|
2018
|
+
const newId = result.sessionId;
|
|
2019
|
+
if (typeof newId !== "string" || !newId) {
|
|
2020
|
+
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
2021
|
+
}
|
|
2022
|
+
return newId;
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Set the active mode for a session.
|
|
2026
|
+
*/
|
|
2027
|
+
async setMode(sessionId, modeId) {
|
|
2028
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2029
|
+
const id = this.allocId();
|
|
2030
|
+
const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
2031
|
+
if (isJsonRpcError(result)) {
|
|
2032
|
+
throw new ACPSessionError("prompt_failed", `session/set_mode failed: ${result.message}`, result);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Set a configuration option for a session.
|
|
2037
|
+
*/
|
|
2038
|
+
async setConfigOption(sessionId, configId, value) {
|
|
2039
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2040
|
+
const id = this.allocId();
|
|
2041
|
+
const result = await this.sendRequest(id, "session/set_config_option", {
|
|
2042
|
+
sessionId,
|
|
2043
|
+
configId,
|
|
2044
|
+
value
|
|
2045
|
+
});
|
|
2046
|
+
if (isJsonRpcError(result)) {
|
|
2047
|
+
throw new ACPSessionError("prompt_failed", `session/set_config_option failed: ${result.message}`, result);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* List available providers and the current provider.
|
|
2052
|
+
*/
|
|
2053
|
+
async listProviders() {
|
|
2054
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2055
|
+
const id = this.allocId();
|
|
2056
|
+
const result = await this.sendRequest(id, "providers/list", {});
|
|
2057
|
+
if (isJsonRpcError(result)) {
|
|
2058
|
+
throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
|
|
2059
|
+
}
|
|
2060
|
+
const r = result;
|
|
2061
|
+
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Send an MCP message to the agent for routing.
|
|
2065
|
+
*/
|
|
2066
|
+
async mcpMessage(connectionId, message) {
|
|
2067
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2068
|
+
const id = this.allocId();
|
|
2069
|
+
const result = await this.sendRequest(id, "mcp/message", { connectionId, message });
|
|
2070
|
+
if (isJsonRpcError(result)) {
|
|
2071
|
+
throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
|
|
2072
|
+
}
|
|
2073
|
+
return result;
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Set the active provider for the agent.
|
|
2077
|
+
*/
|
|
2078
|
+
async setProvider(providerId, config) {
|
|
2079
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2080
|
+
const id = this.allocId();
|
|
2081
|
+
const result = await this.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
2082
|
+
if (isJsonRpcError(result)) {
|
|
2083
|
+
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Disable the current provider.
|
|
2088
|
+
*/
|
|
2089
|
+
async disableProvider() {
|
|
2090
|
+
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2091
|
+
const id = this.allocId();
|
|
2092
|
+
const result = await this.sendRequest(id, "providers/disable", {});
|
|
2093
|
+
if (isJsonRpcError(result)) {
|
|
2094
|
+
throw new ACPSessionError("prompt_failed", `providers/disable failed: ${result.message}`, result);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
2098
|
+
// Prompt
|
|
2099
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
2100
|
+
/**
|
|
2101
|
+
* Run one prompt turn. Creates a session if needed, sends the
|
|
2102
|
+
* prompt, streams session/update notifications, and resolves with
|
|
2103
|
+
* the agent's response.
|
|
2104
|
+
*
|
|
2105
|
+
* @param blocks - Content blocks to send. Use `textContent()` for plain
|
|
2106
|
+
* text, or include ImageContent/AudioContent if the agent's
|
|
2107
|
+
* `promptCapabilities` allow it.
|
|
2108
|
+
* @param signal - AbortSignal for cancellation.
|
|
2109
|
+
*
|
|
2110
|
+
* Cancellation: if `signal` aborts mid-prompt, we send
|
|
2111
|
+
* `session/cancel` (a notification per spec) and keep accepting
|
|
1219
2112
|
* updates until the agent returns with `stopReason: 'cancelled'`.
|
|
1220
2113
|
* The result is the same shape as a normal turn, with
|
|
1221
2114
|
* `stopReason === 'cancelled'`.
|
|
1222
2115
|
*/
|
|
1223
|
-
async prompt(
|
|
2116
|
+
async prompt(blocks, signal, onProgress) {
|
|
1224
2117
|
if (this.closed) {
|
|
1225
2118
|
throw new ACPSessionError("closed", "session is closed");
|
|
1226
2119
|
}
|
|
1227
|
-
if (this.state !== "ready" && this.state !== "done") {
|
|
2120
|
+
if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
|
|
1228
2121
|
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
1229
2122
|
}
|
|
1230
2123
|
if (signal.aborted) {
|
|
1231
|
-
return
|
|
2124
|
+
return emptyRunResult("cancelled");
|
|
1232
2125
|
}
|
|
1233
2126
|
if (!this.sessionId) {
|
|
1234
2127
|
await this.createSession();
|
|
1235
2128
|
}
|
|
1236
2129
|
this.resetScratch();
|
|
2130
|
+
this.progressHandler = onProgress ?? null;
|
|
1237
2131
|
const promptId = this.allocId();
|
|
1238
2132
|
const turnPromise = this.sendRequest(
|
|
1239
2133
|
promptId,
|
|
1240
2134
|
"session/prompt",
|
|
1241
2135
|
{
|
|
1242
2136
|
sessionId: this.sessionId,
|
|
1243
|
-
prompt:
|
|
2137
|
+
prompt: blocks
|
|
1244
2138
|
},
|
|
1245
2139
|
this.timeoutMs
|
|
1246
2140
|
);
|
|
1247
2141
|
let cancelled = false;
|
|
1248
2142
|
const onAbort = () => {
|
|
1249
2143
|
cancelled = true;
|
|
1250
|
-
this.transport.send({
|
|
2144
|
+
this.transport.send({
|
|
2145
|
+
jsonrpc: "2.0",
|
|
2146
|
+
method: "session/cancel",
|
|
2147
|
+
params: { sessionId: this.sessionId }
|
|
2148
|
+
}).catch(() => {
|
|
1251
2149
|
});
|
|
1252
2150
|
};
|
|
1253
2151
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1265,6 +2163,7 @@ var ACPSession = class _ACPSession {
|
|
|
1265
2163
|
throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
|
|
1266
2164
|
} finally {
|
|
1267
2165
|
signal.removeEventListener("abort", onAbort);
|
|
2166
|
+
this.progressHandler = null;
|
|
1268
2167
|
}
|
|
1269
2168
|
this.state = "done";
|
|
1270
2169
|
if (isJsonRpcError(response)) {
|
|
@@ -1277,14 +2176,18 @@ var ACPSession = class _ACPSession {
|
|
|
1277
2176
|
stopReason,
|
|
1278
2177
|
hasText: finalText.length > 0,
|
|
1279
2178
|
usage: this.scratch.usage,
|
|
1280
|
-
plan: this.scratch.plan
|
|
2179
|
+
plan: this.scratch.plan,
|
|
2180
|
+
toolCalls: [...this.scratch.toolCalls.values()],
|
|
2181
|
+
diffs: this.scratch.diffs,
|
|
2182
|
+
thoughts: this.scratch.thoughts
|
|
1281
2183
|
};
|
|
1282
2184
|
}
|
|
1283
2185
|
async createSession() {
|
|
2186
|
+
const servers = this.filterMcpServers(this.opts.mcpServers);
|
|
1284
2187
|
const id = this.allocId();
|
|
1285
2188
|
const result = await this.sendRequest(id, "session/new", {
|
|
1286
2189
|
cwd: this.opts.cwd ?? this.opts.projectRoot,
|
|
1287
|
-
mcpServers:
|
|
2190
|
+
mcpServers: servers
|
|
1288
2191
|
});
|
|
1289
2192
|
if (isJsonRpcError(result)) {
|
|
1290
2193
|
throw new ACPSessionError(
|
|
@@ -1303,12 +2206,40 @@ var ACPSession = class _ACPSession {
|
|
|
1303
2206
|
}
|
|
1304
2207
|
this.sessionId = sessionId;
|
|
1305
2208
|
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Close the current session gracefully (if the agent supports it).
|
|
2211
|
+
*
|
|
2212
|
+
* Sends `session/close` JSON-RPC request, then clears the local
|
|
2213
|
+
* session id. Best-effort — errors are swallowed so the caller can
|
|
2214
|
+
* always proceed to transport teardown.
|
|
2215
|
+
*/
|
|
2216
|
+
async closeSession() {
|
|
2217
|
+
if (!this.sessionId) return;
|
|
2218
|
+
const sid = this.sessionId;
|
|
2219
|
+
this.sessionId = null;
|
|
2220
|
+
if (this.agentCapabilities.sessionCapabilities?.close) {
|
|
2221
|
+
const id = this.allocId();
|
|
2222
|
+
try {
|
|
2223
|
+
await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
|
|
2224
|
+
} catch {
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
2229
|
+
// Lifecycle — close
|
|
2230
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1306
2231
|
/** Tear down the session and kill the child process. */
|
|
1307
2232
|
async close() {
|
|
1308
2233
|
if (this.closed) return;
|
|
1309
2234
|
this.closed = true;
|
|
1310
2235
|
this.state = "closed";
|
|
1311
2236
|
this.terminalServer.releaseAll();
|
|
2237
|
+
if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
|
|
2238
|
+
try {
|
|
2239
|
+
await this.closeSession();
|
|
2240
|
+
} catch {
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
1312
2243
|
for (const [, p] of this.pending) {
|
|
1313
2244
|
clearTimeout(p.timeoutHandle);
|
|
1314
2245
|
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
@@ -1319,6 +2250,24 @@ var ACPSession = class _ACPSession {
|
|
|
1319
2250
|
} catch {
|
|
1320
2251
|
}
|
|
1321
2252
|
}
|
|
2253
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
2254
|
+
// Helpers
|
|
2255
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
2256
|
+
/**
|
|
2257
|
+
* Filter MCP servers according to agent capabilities.
|
|
2258
|
+
* - Stdio servers are always included.
|
|
2259
|
+
* - HTTP servers are only included if agent supports mcpCapabilities.http.
|
|
2260
|
+
* - SSE servers are only included if agent supports mcpCapabilities.sse.
|
|
2261
|
+
*/
|
|
2262
|
+
filterMcpServers(servers) {
|
|
2263
|
+
if (!servers || servers.length === 0) return [];
|
|
2264
|
+
const mcpCaps = this.agentCapabilities.mcpCapabilities ?? {};
|
|
2265
|
+
return servers.filter((s) => {
|
|
2266
|
+
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
2267
|
+
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
2268
|
+
return true;
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
1322
2271
|
// ────────────────────────────────────────────────────────────────────
|
|
1323
2272
|
// Wire layer
|
|
1324
2273
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -1352,6 +2301,28 @@ var ACPSession = class _ACPSession {
|
|
|
1352
2301
|
});
|
|
1353
2302
|
});
|
|
1354
2303
|
}
|
|
2304
|
+
/**
|
|
2305
|
+
* Send a JSON-RPC 2.0 success response to an agent-initiated request.
|
|
2306
|
+
*
|
|
2307
|
+
* Per JSON-RPC 2.0 (and the official ACP SDK's message router) a Response
|
|
2308
|
+
* object MUST carry `jsonrpc: "2.0"` and MUST NOT carry a `method` field —
|
|
2309
|
+
* the SDK classifies any object with a `method` key as a Request and drops
|
|
2310
|
+
* it as a response, so an agent's `fs/*`, `terminal/*`, or
|
|
2311
|
+
* `session/request_permission` callback would hang forever. The legacy
|
|
2312
|
+
* `ACPMessage` type predates v1 (requires `method`, lacks `jsonrpc`), so we
|
|
2313
|
+
* build the correct wire object and cast at the boundary.
|
|
2314
|
+
*/
|
|
2315
|
+
sendResult(id, result) {
|
|
2316
|
+
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
2317
|
+
}
|
|
2318
|
+
/** Send a JSON-RPC 2.0 error response (no `method` field, per spec). */
|
|
2319
|
+
sendErrorResponse(id, code, message) {
|
|
2320
|
+
return this.transport.send({
|
|
2321
|
+
jsonrpc: "2.0",
|
|
2322
|
+
id,
|
|
2323
|
+
error: { code, message }
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
1355
2326
|
handleMessage(msg) {
|
|
1356
2327
|
if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
|
|
1357
2328
|
const pending = this.pending.get(msg.id);
|
|
@@ -1381,6 +2352,23 @@ var ACPSession = class _ACPSession {
|
|
|
1381
2352
|
void this.handleTerminalRequest(msg);
|
|
1382
2353
|
return;
|
|
1383
2354
|
}
|
|
2355
|
+
if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
|
|
2356
|
+
if (msg.id !== void 0) {
|
|
2357
|
+
this.sendResult(msg.id, {}).catch(() => {
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
|
|
2363
|
+
if (msg.id !== void 0) {
|
|
2364
|
+
this.sendResult(msg.id, {}).catch(() => {
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
if (msg.method === "$/cancel_request") {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
1384
2372
|
if (msg.method) {
|
|
1385
2373
|
console.warn(`[acp-session] unhandled method: ${msg.method}`);
|
|
1386
2374
|
}
|
|
@@ -1389,31 +2377,44 @@ var ACPSession = class _ACPSession {
|
|
|
1389
2377
|
const update = msg.params?.update;
|
|
1390
2378
|
if (typeof update !== "object" || update === null) return;
|
|
1391
2379
|
const u = update;
|
|
2380
|
+
this.emitProgress({ type: "raw", update: u });
|
|
1392
2381
|
switch (u.sessionUpdate) {
|
|
1393
2382
|
case "agent_message_chunk": {
|
|
1394
2383
|
const text = extractText(u.content);
|
|
1395
|
-
if (text)
|
|
2384
|
+
if (text) {
|
|
2385
|
+
this.scratch.text += text;
|
|
2386
|
+
this.emitProgress({ type: "message", text });
|
|
2387
|
+
}
|
|
1396
2388
|
return;
|
|
1397
2389
|
}
|
|
1398
|
-
case "thought_chunk":
|
|
2390
|
+
case "thought_chunk": {
|
|
2391
|
+
const text = extractText(u.content);
|
|
2392
|
+
if (text) {
|
|
2393
|
+
this.scratch.thoughts += text;
|
|
2394
|
+
this.emitProgress({ type: "thought", text });
|
|
2395
|
+
}
|
|
1399
2396
|
return;
|
|
2397
|
+
}
|
|
1400
2398
|
case "tool_call":
|
|
1401
|
-
case "tool_call_update":
|
|
2399
|
+
case "tool_call_update": {
|
|
2400
|
+
this.captureToolCall(u, u.sessionUpdate === "tool_call");
|
|
1402
2401
|
return;
|
|
2402
|
+
}
|
|
1403
2403
|
case "plan":
|
|
1404
2404
|
if (Array.isArray(u.entries)) {
|
|
1405
|
-
this.
|
|
2405
|
+
this.scratch.plan = u.entries;
|
|
2406
|
+
this.emitProgress({ type: "plan", entries: u.entries });
|
|
1406
2407
|
}
|
|
1407
2408
|
return;
|
|
1408
2409
|
case "usage_update":
|
|
1409
2410
|
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
1410
|
-
|
|
2411
|
+
const usage = {
|
|
1411
2412
|
used: u.used,
|
|
1412
2413
|
size: u.size,
|
|
1413
|
-
...typeof u.cost === "object" && u.cost !== null ? {
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
});
|
|
2414
|
+
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
2415
|
+
};
|
|
2416
|
+
this.scratch.usage = usage;
|
|
2417
|
+
this.emitProgress({ type: "usage", usage });
|
|
1417
2418
|
}
|
|
1418
2419
|
return;
|
|
1419
2420
|
case "available_commands_update":
|
|
@@ -1421,27 +2422,62 @@ var ACPSession = class _ACPSession {
|
|
|
1421
2422
|
case "config_option_update":
|
|
1422
2423
|
case "session_info_update":
|
|
1423
2424
|
case "user_message_chunk":
|
|
2425
|
+
case "next_edit_suggestions":
|
|
2426
|
+
case "elicitation":
|
|
1424
2427
|
return;
|
|
1425
2428
|
default:
|
|
1426
|
-
console.warn(`[acp-session] unhandled sessionUpdate: ${u.sessionUpdate}`);
|
|
1427
2429
|
return;
|
|
1428
2430
|
}
|
|
1429
2431
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
this.scratch.
|
|
2432
|
+
/**
|
|
2433
|
+
* Fold a `tool_call` / `tool_call_update` notification into the scratch
|
|
2434
|
+
* tool-call map (deduped by toolCallId), extract any `diff` content into
|
|
2435
|
+
* the diffs list, and emit live progress.
|
|
2436
|
+
*/
|
|
2437
|
+
captureToolCall(u, isNew) {
|
|
2438
|
+
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
|
|
2439
|
+
if (!toolCallId) return;
|
|
2440
|
+
const prev = this.scratch.toolCalls.get(toolCallId);
|
|
2441
|
+
const record = {
|
|
2442
|
+
toolCallId,
|
|
2443
|
+
title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
|
|
2444
|
+
kind: typeof u.kind === "string" ? u.kind : prev?.kind,
|
|
2445
|
+
status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
|
|
2446
|
+
rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
|
|
2447
|
+
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
|
|
2448
|
+
};
|
|
2449
|
+
this.scratch.toolCalls.set(toolCallId, record);
|
|
2450
|
+
if (Array.isArray(u.content)) {
|
|
2451
|
+
for (const c of u.content) {
|
|
2452
|
+
if (c && typeof c === "object" && c.type === "diff") {
|
|
2453
|
+
const diff = {
|
|
2454
|
+
path: c.path,
|
|
2455
|
+
oldText: c.oldText,
|
|
2456
|
+
newText: c.newText
|
|
2457
|
+
};
|
|
2458
|
+
this.scratch.diffs.push(diff);
|
|
2459
|
+
this.emitProgress({ type: "diff", diff });
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
this.emitProgress({
|
|
2464
|
+
type: isNew ? "tool_call" : "tool_call_update",
|
|
2465
|
+
toolCall: record
|
|
2466
|
+
});
|
|
1439
2467
|
}
|
|
1440
|
-
|
|
1441
|
-
this.
|
|
2468
|
+
emitProgress(event) {
|
|
2469
|
+
if (!this.progressHandler) return;
|
|
2470
|
+
try {
|
|
2471
|
+
this.progressHandler(event);
|
|
2472
|
+
} catch {
|
|
2473
|
+
}
|
|
1442
2474
|
}
|
|
2475
|
+
/** Live progress handler installed for the duration of a `prompt()` turn. */
|
|
2476
|
+
progressHandler = null;
|
|
2477
|
+
// Per-prompt scratch state
|
|
2478
|
+
scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
1443
2479
|
resetScratch() {
|
|
1444
|
-
this.scratch = { text: "" };
|
|
2480
|
+
this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
1445
2481
|
}
|
|
1446
2482
|
async handlePermissionRequest(msg) {
|
|
1447
2483
|
const id = msg.id;
|
|
@@ -1450,11 +2486,7 @@ var ACPSession = class _ACPSession {
|
|
|
1450
2486
|
const toolCall = params?.toolCall;
|
|
1451
2487
|
const options = Array.isArray(params?.options) ? params.options : [];
|
|
1452
2488
|
if (!toolCall) {
|
|
1453
|
-
await this.
|
|
1454
|
-
id,
|
|
1455
|
-
method: "session/request_permission",
|
|
1456
|
-
error: { code: -32602, message: "toolCall is required" }
|
|
1457
|
-
});
|
|
2489
|
+
await this.sendErrorResponse(id, -32602, "toolCall is required");
|
|
1458
2490
|
return;
|
|
1459
2491
|
}
|
|
1460
2492
|
const policyAbort = new AbortController();
|
|
@@ -1463,22 +2495,14 @@ var ACPSession = class _ACPSession {
|
|
|
1463
2495
|
options,
|
|
1464
2496
|
signal: policyAbort.signal
|
|
1465
2497
|
});
|
|
1466
|
-
await this.
|
|
1467
|
-
id,
|
|
1468
|
-
method: "session/request_permission",
|
|
1469
|
-
result: { outcome }
|
|
1470
|
-
});
|
|
2498
|
+
await this.sendResult(id, { outcome });
|
|
1471
2499
|
}
|
|
1472
2500
|
async handleFsRequest(msg) {
|
|
1473
2501
|
const id = msg.id;
|
|
1474
2502
|
if (id === void 0) return;
|
|
1475
2503
|
const params = msg.params;
|
|
1476
2504
|
if (!params?.path) {
|
|
1477
|
-
await this.
|
|
1478
|
-
id,
|
|
1479
|
-
method: msg.method,
|
|
1480
|
-
error: { code: -32602, message: "path is required" }
|
|
1481
|
-
});
|
|
2505
|
+
await this.sendErrorResponse(id, -32602, "path is required");
|
|
1482
2506
|
return;
|
|
1483
2507
|
}
|
|
1484
2508
|
try {
|
|
@@ -1487,19 +2511,19 @@ var ACPSession = class _ACPSession {
|
|
|
1487
2511
|
sessionId: params.sessionId ?? "",
|
|
1488
2512
|
path: params.path
|
|
1489
2513
|
});
|
|
1490
|
-
await this.
|
|
2514
|
+
await this.sendResult(id, result);
|
|
1491
2515
|
} else {
|
|
1492
2516
|
await this.fileServer.writeTextFile({
|
|
1493
2517
|
sessionId: params.sessionId ?? "",
|
|
1494
2518
|
path: params.path,
|
|
1495
2519
|
content: params.content ?? ""
|
|
1496
2520
|
});
|
|
1497
|
-
await this.
|
|
2521
|
+
await this.sendResult(id, {});
|
|
1498
2522
|
}
|
|
1499
2523
|
} catch (err) {
|
|
1500
2524
|
const code = err instanceof FsError ? -32602 : -32603;
|
|
1501
2525
|
const message = err instanceof Error ? err.message : String(err);
|
|
1502
|
-
await this.
|
|
2526
|
+
await this.sendErrorResponse(id, code, message);
|
|
1503
2527
|
}
|
|
1504
2528
|
}
|
|
1505
2529
|
async handleTerminalRequest(msg) {
|
|
@@ -1524,192 +2548,73 @@ var ACPSession = class _ACPSession {
|
|
|
1524
2548
|
createOpts.outputByteLimit = params.outputByteLimit;
|
|
1525
2549
|
}
|
|
1526
2550
|
const result = this.terminalServer.create(createOpts);
|
|
1527
|
-
await this.
|
|
2551
|
+
await this.sendResult(id, result);
|
|
1528
2552
|
return;
|
|
1529
2553
|
}
|
|
1530
2554
|
case "terminal/output": {
|
|
1531
2555
|
const terminalId = String(params.terminalId ?? "");
|
|
1532
2556
|
const out = this.terminalServer.output(terminalId);
|
|
1533
|
-
await this.
|
|
2557
|
+
await this.sendResult(id, out);
|
|
1534
2558
|
return;
|
|
1535
2559
|
}
|
|
1536
2560
|
case "terminal/wait_for_exit": {
|
|
1537
2561
|
const terminalId = String(params.terminalId ?? "");
|
|
1538
2562
|
const exit = await this.terminalServer.waitForExit(terminalId);
|
|
1539
|
-
await this.
|
|
2563
|
+
await this.sendResult(id, exit);
|
|
1540
2564
|
return;
|
|
1541
2565
|
}
|
|
1542
2566
|
case "terminal/kill": {
|
|
1543
2567
|
const terminalId = String(params.terminalId ?? "");
|
|
1544
2568
|
this.terminalServer.kill(terminalId);
|
|
1545
|
-
await this.
|
|
2569
|
+
await this.sendResult(id, {});
|
|
1546
2570
|
return;
|
|
1547
2571
|
}
|
|
1548
2572
|
case "terminal/release": {
|
|
1549
2573
|
const terminalId = String(params.terminalId ?? "");
|
|
1550
2574
|
this.terminalServer.release(terminalId);
|
|
1551
|
-
await this.
|
|
2575
|
+
await this.sendResult(id, {});
|
|
1552
2576
|
return;
|
|
1553
2577
|
}
|
|
1554
2578
|
default:
|
|
1555
|
-
await this.
|
|
1556
|
-
id,
|
|
1557
|
-
method: msg.method,
|
|
1558
|
-
error: { code: -32601, message: `unknown method: ${msg.method}` }
|
|
1559
|
-
});
|
|
2579
|
+
await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
1560
2580
|
}
|
|
1561
2581
|
} catch (err) {
|
|
1562
2582
|
const message = err instanceof Error ? err.message : String(err);
|
|
1563
|
-
await this.
|
|
1564
|
-
id,
|
|
1565
|
-
method: msg.method,
|
|
1566
|
-
error: { code: -32603, message }
|
|
1567
|
-
});
|
|
2583
|
+
await this.sendErrorResponse(id, -32603, message);
|
|
1568
2584
|
}
|
|
1569
2585
|
}
|
|
1570
2586
|
};
|
|
1571
2587
|
function textContent(text) {
|
|
1572
2588
|
return { type: "text", text };
|
|
1573
2589
|
}
|
|
2590
|
+
function imageContent(mimeType, data) {
|
|
2591
|
+
return { type: "image", mimeType, data };
|
|
2592
|
+
}
|
|
2593
|
+
function audioContent(mimeType, data) {
|
|
2594
|
+
return { type: "audio", mimeType, data };
|
|
2595
|
+
}
|
|
1574
2596
|
function extractText(block) {
|
|
1575
2597
|
if (typeof block !== "object" || block === null) return "";
|
|
1576
2598
|
const b = block;
|
|
1577
2599
|
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
// src/integration/acp-subagent-runner.ts
|
|
1582
|
-
var ACP_AGENT_COMMANDS = {
|
|
1583
|
-
cline: {
|
|
1584
|
-
command: "npx",
|
|
1585
|
-
args: ["-y", "@agentify/cline"],
|
|
1586
|
-
role: "cline"
|
|
1587
|
-
},
|
|
1588
|
-
"gemini-cli": {
|
|
1589
|
-
command: "gemini",
|
|
1590
|
-
role: "gemini-cli"
|
|
1591
|
-
},
|
|
1592
|
-
copilot: {
|
|
1593
|
-
command: "gh",
|
|
1594
|
-
args: ["copilot", "agent"],
|
|
1595
|
-
role: "copilot"
|
|
1596
|
-
},
|
|
1597
|
-
openhands: {
|
|
1598
|
-
command: "openhands",
|
|
1599
|
-
role: "openhands"
|
|
1600
|
-
},
|
|
1601
|
-
goose: {
|
|
1602
|
-
command: "goose",
|
|
1603
|
-
role: "goose"
|
|
2600
|
+
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
2601
|
+
return b.resource.text;
|
|
1604
2602
|
}
|
|
1605
|
-
|
|
1606
|
-
async function makeACPSubagentRunner(options) {
|
|
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();
|
|
1613
|
-
}
|
|
1614
|
-
};
|
|
1615
|
-
return wrappedRunner;
|
|
2603
|
+
return "";
|
|
1616
2604
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
1620
|
-
const runner = async (task, ctx) => {
|
|
1621
|
-
let session = null;
|
|
1622
|
-
try {
|
|
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
|
|
1631
|
-
});
|
|
1632
|
-
} catch (err) {
|
|
1633
|
-
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
1634
|
-
}
|
|
1635
|
-
try {
|
|
1636
|
-
const result = await session.prompt(task.description, ctx.signal);
|
|
1637
|
-
return {
|
|
1638
|
-
result: result.text,
|
|
1639
|
-
iterations: 1,
|
|
1640
|
-
toolCalls: 0
|
|
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
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
};
|
|
1654
|
-
const stop = () => {
|
|
1655
|
-
};
|
|
1656
|
-
return { runner, stop };
|
|
2605
|
+
function isRecord(v) {
|
|
2606
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1657
2607
|
}
|
|
1658
|
-
function
|
|
1659
|
-
if (err instanceof ACPSessionError) {
|
|
1660
|
-
const kind = mapACPKind(err.kind);
|
|
1661
|
-
return {
|
|
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
|
-
}
|
|
1670
|
-
};
|
|
1671
|
-
}
|
|
1672
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2608
|
+
function emptyRunResult(stopReason) {
|
|
1673
2609
|
return {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
...err instanceof Error && err.stack !== void 0 ? { stack: err.stack } : {}
|
|
1681
|
-
}
|
|
2610
|
+
text: "",
|
|
2611
|
+
stopReason,
|
|
2612
|
+
hasText: false,
|
|
2613
|
+
toolCalls: [],
|
|
2614
|
+
diffs: [],
|
|
2615
|
+
thoughts: ""
|
|
1682
2616
|
};
|
|
1683
2617
|
}
|
|
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
2618
|
|
|
1714
2619
|
// src/registry/agents.catalog.ts
|
|
1715
2620
|
var AGENTS_CATALOG = [
|
|
@@ -1719,9 +2624,12 @@ var AGENTS_CATALOG = [
|
|
|
1719
2624
|
displayName: "Claude Code",
|
|
1720
2625
|
vendor: "anthropic",
|
|
1721
2626
|
probe: { command: "claude", args: ["--version"] },
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1724
|
-
acp
|
|
2627
|
+
// Claude Code does not speak stdio ACP from the bare `claude` binary —
|
|
2628
|
+
// it drops into its interactive TUI. The official ACP adapter
|
|
2629
|
+
// (`@agentclientprotocol/claude-agent-acp`, registry id `claude-acp`)
|
|
2630
|
+
// wraps the logged-in Claude Code CLI and translates ACP ↔ Claude Code.
|
|
2631
|
+
// Verify with `/acp probe claude-code`; override via `config.acp.agents`.
|
|
2632
|
+
acp: { command: "npx", args: ["-y", "@agentclientprotocol/claude-agent-acp"] },
|
|
1725
2633
|
supports: {
|
|
1726
2634
|
loadSession: true,
|
|
1727
2635
|
promptImages: true,
|
|
@@ -1737,7 +2645,10 @@ var AGENTS_CATALOG = [
|
|
|
1737
2645
|
displayName: "Gemini CLI",
|
|
1738
2646
|
vendor: "google",
|
|
1739
2647
|
probe: { command: "gemini", args: ["--version"] },
|
|
1740
|
-
|
|
2648
|
+
// Gemini CLI (the @google/gemini-cli package, registry id `gemini`)
|
|
2649
|
+
// speaks ACP behind `--acp`. We invoke the locally-installed binary so it
|
|
2650
|
+
// uses the user's existing login. Confirm with `/acp probe gemini-cli`.
|
|
2651
|
+
acp: { command: "gemini", args: ["--acp"] },
|
|
1741
2652
|
supports: {
|
|
1742
2653
|
loadSession: true,
|
|
1743
2654
|
promptImages: true,
|
|
@@ -1753,7 +2664,10 @@ var AGENTS_CATALOG = [
|
|
|
1753
2664
|
displayName: "Codex CLI",
|
|
1754
2665
|
vendor: "openai",
|
|
1755
2666
|
probe: { command: "codex", args: ["--version"] },
|
|
1756
|
-
|
|
2667
|
+
// Bare `codex` has no stdio-ACP entry; the official adapter
|
|
2668
|
+
// (`@agentclientprotocol/codex-acp`, registry id `codex-acp`) wraps the
|
|
2669
|
+
// logged-in Codex CLI. Confirm with `/acp probe codex-cli`.
|
|
2670
|
+
acp: { command: "npx", args: ["-y", "@agentclientprotocol/codex-acp"] },
|
|
1757
2671
|
supports: {
|
|
1758
2672
|
loadSession: false,
|
|
1759
2673
|
promptImages: false,
|
|
@@ -1769,7 +2683,9 @@ var AGENTS_CATALOG = [
|
|
|
1769
2683
|
displayName: "GitHub Copilot CLI",
|
|
1770
2684
|
vendor: "github",
|
|
1771
2685
|
probe: { command: "gh", args: ["copilot", "--help"] },
|
|
1772
|
-
|
|
2686
|
+
// ACP is in the standalone @github/copilot CLI (registry id
|
|
2687
|
+
// `github-copilot-cli`), not the `gh copilot` extension. Use the package.
|
|
2688
|
+
acp: { command: "npx", args: ["-y", "@github/copilot", "--acp"] },
|
|
1773
2689
|
supports: {
|
|
1774
2690
|
loadSession: false,
|
|
1775
2691
|
promptImages: false,
|
|
@@ -1785,9 +2701,10 @@ var AGENTS_CATALOG = [
|
|
|
1785
2701
|
displayName: "Cline",
|
|
1786
2702
|
vendor: "community",
|
|
1787
2703
|
probe: { command: "npx", args: ["--version"] },
|
|
2704
|
+
// Registry id `cline`: the `cline` npm package speaks ACP behind `--acp`.
|
|
1788
2705
|
acp: {
|
|
1789
2706
|
command: "npx",
|
|
1790
|
-
args: ["-y", "
|
|
2707
|
+
args: ["-y", "cline", "--acp"]
|
|
1791
2708
|
},
|
|
1792
2709
|
supports: {
|
|
1793
2710
|
loadSession: true,
|
|
@@ -1803,7 +2720,7 @@ var AGENTS_CATALOG = [
|
|
|
1803
2720
|
displayName: "Goose",
|
|
1804
2721
|
vendor: "community",
|
|
1805
2722
|
probe: { command: "goose", args: ["--version"] },
|
|
1806
|
-
acp: { command: "goose", args: [] },
|
|
2723
|
+
acp: { command: "goose", args: ["acp"] },
|
|
1807
2724
|
supports: {
|
|
1808
2725
|
loadSession: true,
|
|
1809
2726
|
promptImages: true,
|
|
@@ -1835,7 +2752,8 @@ var AGENTS_CATALOG = [
|
|
|
1835
2752
|
displayName: "Qwen Code",
|
|
1836
2753
|
vendor: "community",
|
|
1837
2754
|
probe: { command: "qwen", args: ["--version"] },
|
|
1838
|
-
|
|
2755
|
+
// Qwen Code (the @qwen-code/qwen-code package) speaks ACP behind `--acp`.
|
|
2756
|
+
acp: { command: "qwen", args: ["--acp"] },
|
|
1839
2757
|
supports: {
|
|
1840
2758
|
loadSession: false,
|
|
1841
2759
|
promptImages: false,
|
|
@@ -1865,7 +2783,8 @@ var AGENTS_CATALOG = [
|
|
|
1865
2783
|
displayName: "OpenCode",
|
|
1866
2784
|
vendor: "community",
|
|
1867
2785
|
probe: { command: "opencode", args: ["--version"] },
|
|
1868
|
-
|
|
2786
|
+
// OpenCode speaks ACP via its `acp` subcommand (registry id `opencode`).
|
|
2787
|
+
acp: { command: "opencode", args: ["acp"] },
|
|
1869
2788
|
supports: {
|
|
1870
2789
|
loadSession: true,
|
|
1871
2790
|
promptImages: true,
|
|
@@ -1895,7 +2814,8 @@ var AGENTS_CATALOG = [
|
|
|
1895
2814
|
displayName: "Cursor",
|
|
1896
2815
|
vendor: "community",
|
|
1897
2816
|
probe: { command: "cursor", args: ["--version"] },
|
|
1898
|
-
|
|
2817
|
+
// Cursor's ACP entry is the `cursor-agent acp` binary (registry id `cursor`).
|
|
2818
|
+
acp: { command: "cursor-agent", args: ["acp"] },
|
|
1899
2819
|
supports: {
|
|
1900
2820
|
loadSession: true,
|
|
1901
2821
|
promptImages: true,
|
|
@@ -1909,8 +2829,369 @@ var AGENTS_CATALOG = [
|
|
|
1909
2829
|
function findAgentDescriptor(id) {
|
|
1910
2830
|
return AGENTS_CATALOG.find((a) => a.id === id);
|
|
1911
2831
|
}
|
|
2832
|
+
|
|
2833
|
+
// src/integration/acp-subagent-runner.ts
|
|
2834
|
+
var ACP_AGENT_COMMANDS = {
|
|
2835
|
+
cline: {
|
|
2836
|
+
command: "npx",
|
|
2837
|
+
args: ["-y", "@agentify/cline"],
|
|
2838
|
+
role: "cline"
|
|
2839
|
+
},
|
|
2840
|
+
"gemini-cli": {
|
|
2841
|
+
command: "gemini",
|
|
2842
|
+
role: "gemini-cli"
|
|
2843
|
+
},
|
|
2844
|
+
copilot: {
|
|
2845
|
+
command: "gh",
|
|
2846
|
+
args: ["copilot", "agent"],
|
|
2847
|
+
role: "copilot"
|
|
2848
|
+
},
|
|
2849
|
+
openhands: {
|
|
2850
|
+
command: "openhands",
|
|
2851
|
+
role: "openhands"
|
|
2852
|
+
},
|
|
2853
|
+
goose: {
|
|
2854
|
+
command: "goose",
|
|
2855
|
+
role: "goose"
|
|
2856
|
+
}
|
|
2857
|
+
};
|
|
2858
|
+
async function makeACPSubagentRunner(options) {
|
|
2859
|
+
const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
|
|
2860
|
+
const wrappedRunner = async (task, ctx) => {
|
|
2861
|
+
try {
|
|
2862
|
+
return await runner(task, ctx);
|
|
2863
|
+
} finally {
|
|
2864
|
+
stop();
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
return wrappedRunner;
|
|
2868
|
+
}
|
|
2869
|
+
async function makeACPSubagentRunnerWithStop(options) {
|
|
2870
|
+
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
2871
|
+
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
2872
|
+
const persistent = options.persistent === true;
|
|
2873
|
+
let shared = null;
|
|
2874
|
+
const startSession = async () => {
|
|
2875
|
+
return ACPSession.start({
|
|
2876
|
+
command: options.command,
|
|
2877
|
+
...options.args !== void 0 ? { args: options.args } : {},
|
|
2878
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
2879
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
2880
|
+
projectRoot,
|
|
2881
|
+
timeoutMs,
|
|
2882
|
+
role: options.role,
|
|
2883
|
+
...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
|
|
2884
|
+
...options.mcpServers !== void 0 ? { mcpServers: options.mcpServers } : {}
|
|
2885
|
+
});
|
|
2886
|
+
};
|
|
2887
|
+
const runner = async (task, ctx) => {
|
|
2888
|
+
let session;
|
|
2889
|
+
const reuse = persistent && shared !== null;
|
|
2890
|
+
try {
|
|
2891
|
+
session = reuse ? shared : await startSession();
|
|
2892
|
+
if (persistent) shared = session;
|
|
2893
|
+
} catch (err) {
|
|
2894
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
2895
|
+
}
|
|
2896
|
+
const onProgress = (event) => {
|
|
2897
|
+
try {
|
|
2898
|
+
ctx.budget.markActivity();
|
|
2899
|
+
} catch {
|
|
2900
|
+
}
|
|
2901
|
+
options.onProgress?.(event);
|
|
2902
|
+
};
|
|
2903
|
+
try {
|
|
2904
|
+
const result = await session.prompt(
|
|
2905
|
+
[textContent(task.description)],
|
|
2906
|
+
ctx.signal,
|
|
2907
|
+
onProgress
|
|
2908
|
+
);
|
|
2909
|
+
return {
|
|
2910
|
+
result: result.text,
|
|
2911
|
+
iterations: 1,
|
|
2912
|
+
toolCalls: result.toolCalls.length
|
|
2913
|
+
};
|
|
2914
|
+
} catch (err) {
|
|
2915
|
+
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
2916
|
+
} finally {
|
|
2917
|
+
if (!persistent) {
|
|
2918
|
+
try {
|
|
2919
|
+
await session.close();
|
|
2920
|
+
} catch {
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
};
|
|
2925
|
+
const stop = async () => {
|
|
2926
|
+
if (shared) {
|
|
2927
|
+
const s = shared;
|
|
2928
|
+
shared = null;
|
|
2929
|
+
try {
|
|
2930
|
+
await s.close();
|
|
2931
|
+
} catch {
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
return { runner, stop };
|
|
2936
|
+
}
|
|
2937
|
+
function acpErrorToSubagentError(err, subagentId) {
|
|
2938
|
+
if (err instanceof ACPSessionError) {
|
|
2939
|
+
const kind = mapACPKind(err.kind);
|
|
2940
|
+
return {
|
|
2941
|
+
kind,
|
|
2942
|
+
message: `${subagentId}: ${err.message}`,
|
|
2943
|
+
retryable: isRetryable(kind),
|
|
2944
|
+
cause: {
|
|
2945
|
+
name: err.name,
|
|
2946
|
+
message: err.message,
|
|
2947
|
+
...err.stack !== void 0 ? { stack: err.stack } : {}
|
|
2948
|
+
}
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2952
|
+
return {
|
|
2953
|
+
kind: "bridge_failed",
|
|
2954
|
+
message: `${subagentId}: ${message}`,
|
|
2955
|
+
retryable: false,
|
|
2956
|
+
cause: {
|
|
2957
|
+
name: err instanceof Error ? err.name : "Error",
|
|
2958
|
+
message,
|
|
2959
|
+
...err instanceof Error && err.stack !== void 0 ? { stack: err.stack } : {}
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
function mapACPKind(acpKind) {
|
|
2964
|
+
switch (acpKind) {
|
|
2965
|
+
case "spawn_failed":
|
|
2966
|
+
case "init_failed":
|
|
2967
|
+
case "session_create_failed":
|
|
2968
|
+
case "agent_died":
|
|
2969
|
+
case "protocol_error":
|
|
2970
|
+
return "bridge_failed";
|
|
2971
|
+
case "prompt_failed":
|
|
2972
|
+
return "tool_failed";
|
|
2973
|
+
case "auth_failed":
|
|
2974
|
+
case "logout_failed":
|
|
2975
|
+
return "bridge_failed";
|
|
2976
|
+
case "aborted":
|
|
2977
|
+
return "aborted_by_parent";
|
|
2978
|
+
case "closed":
|
|
2979
|
+
case "unsupported_capability":
|
|
2980
|
+
return "unknown";
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
function isRetryable(kind) {
|
|
2984
|
+
switch (kind) {
|
|
2985
|
+
case "provider_5xx":
|
|
2986
|
+
case "provider_rate_limit":
|
|
2987
|
+
case "provider_timeout":
|
|
2988
|
+
case "tool_threw":
|
|
2989
|
+
case "budget_timeout":
|
|
2990
|
+
return true;
|
|
2991
|
+
default:
|
|
2992
|
+
return false;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
var REGISTRY_ID_ALIASES = {
|
|
2996
|
+
"claude-code": "claude-acp",
|
|
2997
|
+
"gemini-cli": "gemini",
|
|
2998
|
+
"codex-cli": "codex-acp",
|
|
2999
|
+
copilot: "github-copilot-cli"
|
|
3000
|
+
};
|
|
3001
|
+
function resolveAcpAgentCommand(id, overrides, live) {
|
|
3002
|
+
const ov = overrides?.[id];
|
|
3003
|
+
if (ov && typeof ov.command === "string" && ov.command.length > 0) {
|
|
3004
|
+
const out = {
|
|
3005
|
+
command: ov.command,
|
|
3006
|
+
args: [...ov.args ?? []],
|
|
3007
|
+
role: id
|
|
3008
|
+
};
|
|
3009
|
+
if (ov.env) out.env = ov.env;
|
|
3010
|
+
return out;
|
|
3011
|
+
}
|
|
3012
|
+
const desc = findAgentDescriptor(id);
|
|
3013
|
+
if (desc) {
|
|
3014
|
+
const out = {
|
|
3015
|
+
command: desc.acp.command,
|
|
3016
|
+
args: [...desc.acp.args ?? []],
|
|
3017
|
+
role: id
|
|
3018
|
+
};
|
|
3019
|
+
if (desc.acp.env) out.env = desc.acp.env;
|
|
3020
|
+
return out;
|
|
3021
|
+
}
|
|
3022
|
+
const liveEntry = live?.[id] ?? live?.[REGISTRY_ID_ALIASES[id] ?? ""];
|
|
3023
|
+
if (liveEntry && typeof liveEntry.command === "string" && liveEntry.command.length > 0) {
|
|
3024
|
+
const out = {
|
|
3025
|
+
command: liveEntry.command,
|
|
3026
|
+
args: [...liveEntry.args ?? []],
|
|
3027
|
+
role: id
|
|
3028
|
+
};
|
|
3029
|
+
if (liveEntry.env) out.env = liveEntry.env;
|
|
3030
|
+
return out;
|
|
3031
|
+
}
|
|
3032
|
+
const fromMap = ACP_AGENT_COMMANDS[id];
|
|
3033
|
+
if (fromMap) return fromMap;
|
|
3034
|
+
return null;
|
|
3035
|
+
}
|
|
3036
|
+
async function runOneAcpTask(opts) {
|
|
3037
|
+
const role = opts.role ?? "acp";
|
|
3038
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
3039
|
+
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
3040
|
+
command: opts.command,
|
|
3041
|
+
...opts.args !== void 0 ? { args: opts.args } : {},
|
|
3042
|
+
...opts.env !== void 0 ? { env: opts.env } : {},
|
|
3043
|
+
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
3044
|
+
...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
|
|
3045
|
+
role,
|
|
3046
|
+
timeoutMs,
|
|
3047
|
+
...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
|
|
3048
|
+
...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
|
|
3049
|
+
});
|
|
3050
|
+
try {
|
|
3051
|
+
const budget = new SubagentBudget({
|
|
3052
|
+
timeoutMs,
|
|
3053
|
+
maxIterations: 2e3,
|
|
3054
|
+
maxToolCalls: 5e3
|
|
3055
|
+
});
|
|
3056
|
+
budget.start();
|
|
3057
|
+
const ctx = {
|
|
3058
|
+
subagentId: role,
|
|
3059
|
+
config: { id: role, name: role, role, provider: "acp", prompt: "" },
|
|
3060
|
+
budget,
|
|
3061
|
+
signal: opts.signal ?? new AbortController().signal,
|
|
3062
|
+
bridge: null
|
|
3063
|
+
};
|
|
3064
|
+
const result = await runner({ id: `acp-${role}`, description: opts.task }, ctx);
|
|
3065
|
+
return {
|
|
3066
|
+
result: result.result == null ? "" : String(result.result),
|
|
3067
|
+
iterations: result.iterations,
|
|
3068
|
+
toolCalls: result.toolCalls
|
|
3069
|
+
};
|
|
3070
|
+
} finally {
|
|
3071
|
+
try {
|
|
3072
|
+
await stop();
|
|
3073
|
+
} catch {
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
async function probeAcpAgent(idOrCmd, opts) {
|
|
3078
|
+
const id = typeof idOrCmd === "string" ? idOrCmd : idOrCmd.role ?? idOrCmd.command;
|
|
3079
|
+
const cmd = typeof idOrCmd === "string" ? resolveAcpAgentCommand(idOrCmd, opts?.overrides, opts?.live) : idOrCmd;
|
|
3080
|
+
if (!cmd) return { id, ok: false, ms: 0, error: "unknown agent" };
|
|
3081
|
+
const timeoutMs = opts?.timeoutMs ?? 8e3;
|
|
3082
|
+
const startedAt = Date.now();
|
|
3083
|
+
let session = null;
|
|
3084
|
+
try {
|
|
3085
|
+
session = await ACPSession.start({
|
|
3086
|
+
command: cmd.command,
|
|
3087
|
+
...cmd.args !== void 0 ? { args: cmd.args } : {},
|
|
3088
|
+
...cmd.env !== void 0 ? { env: cmd.env } : {},
|
|
3089
|
+
projectRoot: opts?.projectRoot ?? process.cwd(),
|
|
3090
|
+
// Bounds the `initialize` request: a CLI that spawns but never answers
|
|
3091
|
+
// the handshake fails after this instead of blocking.
|
|
3092
|
+
timeoutMs
|
|
3093
|
+
});
|
|
3094
|
+
const info = session.getAgentInfo();
|
|
3095
|
+
return {
|
|
3096
|
+
id,
|
|
3097
|
+
ok: true,
|
|
3098
|
+
ms: Date.now() - startedAt,
|
|
3099
|
+
...info ? { agentInfo: info } : {}
|
|
3100
|
+
};
|
|
3101
|
+
} catch (err) {
|
|
3102
|
+
return {
|
|
3103
|
+
id,
|
|
3104
|
+
ok: false,
|
|
3105
|
+
ms: Date.now() - startedAt,
|
|
3106
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3107
|
+
};
|
|
3108
|
+
} finally {
|
|
3109
|
+
if (session) {
|
|
3110
|
+
try {
|
|
3111
|
+
await session.close();
|
|
3112
|
+
} catch {
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
async function probeAcpAgents(opts) {
|
|
3118
|
+
const localTimeout = opts.timeoutMs ?? 2e4;
|
|
3119
|
+
const pkgTimeout = opts.packageTimeoutMs ?? 9e4;
|
|
3120
|
+
const ids = opts.agentIds;
|
|
3121
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3122
|
+
const local = [];
|
|
3123
|
+
const pkg = [];
|
|
3124
|
+
const cmds = /* @__PURE__ */ new Map();
|
|
3125
|
+
for (const id of ids) {
|
|
3126
|
+
const cmd = opts.resolveCmd(id);
|
|
3127
|
+
cmds.set(id, cmd);
|
|
3128
|
+
if (!cmd) continue;
|
|
3129
|
+
if (cmd.command === "npx" || cmd.command === "uvx") pkg.push(id);
|
|
3130
|
+
else local.push(id);
|
|
3131
|
+
}
|
|
3132
|
+
const runPhase = async (phaseIds, concurrency, timeoutMs) => {
|
|
3133
|
+
let next = 0;
|
|
3134
|
+
const workerCount = Math.min(Math.max(1, concurrency), Math.max(1, phaseIds.length));
|
|
3135
|
+
const workers = [];
|
|
3136
|
+
for (let w = 0; w < workerCount; w++) {
|
|
3137
|
+
workers.push(
|
|
3138
|
+
(async () => {
|
|
3139
|
+
while (true) {
|
|
3140
|
+
const current = next++;
|
|
3141
|
+
if (current >= phaseIds.length) return;
|
|
3142
|
+
const id = phaseIds[current];
|
|
3143
|
+
if (opts.signal?.aborted) {
|
|
3144
|
+
byId.set(id, { id, ok: false, ms: 0, error: "aborted" });
|
|
3145
|
+
continue;
|
|
3146
|
+
}
|
|
3147
|
+
const cmd = cmds.get(id);
|
|
3148
|
+
const r = await probeAcpAgent(cmd, {
|
|
3149
|
+
timeoutMs,
|
|
3150
|
+
...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {}
|
|
3151
|
+
});
|
|
3152
|
+
r.id = id;
|
|
3153
|
+
byId.set(id, r);
|
|
3154
|
+
opts.onProgress?.(id, r);
|
|
3155
|
+
}
|
|
3156
|
+
})()
|
|
3157
|
+
);
|
|
3158
|
+
}
|
|
3159
|
+
await Promise.all(workers);
|
|
3160
|
+
};
|
|
3161
|
+
for (const id of ids) {
|
|
3162
|
+
if (cmds.get(id) === null) {
|
|
3163
|
+
const r = { id, ok: false, ms: 0, error: "unknown agent" };
|
|
3164
|
+
byId.set(id, r);
|
|
3165
|
+
opts.onProgress?.(id, r);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
await runPhase(local, opts.concurrency ?? 4, localTimeout);
|
|
3169
|
+
await runPhase(pkg, 2, pkgTimeout);
|
|
3170
|
+
return ids.map((id) => byId.get(id) ?? { id, ok: false, ms: 0, error: "not probed" });
|
|
3171
|
+
}
|
|
1912
3172
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
1913
3173
|
var PROBE_CACHE_MS = 5e3;
|
|
3174
|
+
var MAX_PARALLEL_PROBES = 4;
|
|
3175
|
+
async function probeWithBound(items, worker, limit) {
|
|
3176
|
+
const safeLimit = Math.max(1, Math.min(limit, items.length));
|
|
3177
|
+
const results = new Array(items.length);
|
|
3178
|
+
let nextIndex = 0;
|
|
3179
|
+
const workerCount = Math.min(safeLimit, items.length);
|
|
3180
|
+
const runners = [];
|
|
3181
|
+
for (let i = 0; i < workerCount; i++) {
|
|
3182
|
+
runners.push(
|
|
3183
|
+
(async () => {
|
|
3184
|
+
while (true) {
|
|
3185
|
+
const current = nextIndex++;
|
|
3186
|
+
if (current >= items.length) return;
|
|
3187
|
+
results[current] = await worker(items[current]);
|
|
3188
|
+
}
|
|
3189
|
+
})()
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
await Promise.all(runners);
|
|
3193
|
+
return results;
|
|
3194
|
+
}
|
|
1914
3195
|
async function defaultProbe(desc, timeoutMs) {
|
|
1915
3196
|
const start = Date.now();
|
|
1916
3197
|
return new Promise((resolve3) => {
|
|
@@ -2010,13 +3291,21 @@ var EnsembleRegistry = class {
|
|
|
2010
3291
|
/**
|
|
2011
3292
|
* Probe every catalog entry in parallel and return the detection
|
|
2012
3293
|
* results. Results are cached for `PROBE_CACHE_MS`.
|
|
3294
|
+
*
|
|
3295
|
+
* Probes are dispatched with bounded concurrency (`MAX_PARALLEL_PROBES`)
|
|
3296
|
+
* so cold-start lists do not spawn `catalog.length` subprocesses at
|
|
3297
|
+
* once — which is especially wasteful on Windows shells and on hosts
|
|
3298
|
+
* with large agent catalogs. The output order still matches
|
|
3299
|
+
* `this.catalog` so callers can index by position.
|
|
2013
3300
|
*/
|
|
2014
3301
|
async list() {
|
|
2015
3302
|
if (this.cache && Date.now() - this.cache.at < PROBE_CACHE_MS) {
|
|
2016
3303
|
return this.cache.result;
|
|
2017
3304
|
}
|
|
2018
|
-
const result = await
|
|
2019
|
-
this.catalog
|
|
3305
|
+
const result = await probeWithBound(
|
|
3306
|
+
this.catalog,
|
|
3307
|
+
(d) => this.detect(d),
|
|
3308
|
+
MAX_PARALLEL_PROBES
|
|
2020
3309
|
);
|
|
2021
3310
|
this.cache = { at: Date.now(), result };
|
|
2022
3311
|
return result;
|
|
@@ -2045,31 +3334,126 @@ var EnsembleRegistry = class {
|
|
|
2045
3334
|
return all.filter((a) => a.installed);
|
|
2046
3335
|
}
|
|
2047
3336
|
};
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
const
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
3337
|
+
|
|
3338
|
+
// src/registry/acp-registry-fetch.ts
|
|
3339
|
+
var ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
3340
|
+
function currentPlatformKey() {
|
|
3341
|
+
const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux";
|
|
3342
|
+
const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : process.arch;
|
|
3343
|
+
return `${os}-${arch}`;
|
|
3344
|
+
}
|
|
3345
|
+
function basename(cmd) {
|
|
3346
|
+
const cleaned = cmd.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
3347
|
+
const parts = cleaned.split("/");
|
|
3348
|
+
return parts[parts.length - 1] || cleaned;
|
|
3349
|
+
}
|
|
3350
|
+
function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
|
|
3351
|
+
if (!entry || typeof entry.id !== "string" || entry.id.length === 0) return null;
|
|
3352
|
+
const dist = entry.distribution;
|
|
3353
|
+
let acp = null;
|
|
3354
|
+
if (dist?.npx?.package) {
|
|
3355
|
+
acp = { command: "npx", args: ["-y", dist.npx.package, ...dist.npx.args ?? []] };
|
|
3356
|
+
} else if (dist?.uvx?.package) {
|
|
3357
|
+
acp = { command: "uvx", args: [dist.uvx.package, ...dist.uvx.args ?? []] };
|
|
3358
|
+
} else if (dist?.binary) {
|
|
3359
|
+
const target = dist.binary[platformKey];
|
|
3360
|
+
if (target?.cmd) {
|
|
3361
|
+
acp = {
|
|
3362
|
+
command: basename(target.cmd),
|
|
3363
|
+
args: [...target.args ?? []],
|
|
3364
|
+
...target.env ? { env: target.env } : {}
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
if (!acp) return null;
|
|
3369
|
+
const probeCmd = acp.command === "npx" ? "npx" : acp.command === "uvx" ? "uvx" : acp.command;
|
|
3370
|
+
return {
|
|
3371
|
+
id: entry.id,
|
|
3372
|
+
displayName: entry.name ?? entry.id,
|
|
3373
|
+
vendor: inferVendor(entry),
|
|
3374
|
+
probe: { command: probeCmd, args: ["--version"] },
|
|
3375
|
+
acp,
|
|
3376
|
+
supports: { loadSession: true, promptImages: true, terminal: true, fs: true },
|
|
3377
|
+
integration: "native",
|
|
3378
|
+
docs: entry.repository ?? entry.website ?? ""
|
|
2057
3379
|
};
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
};
|
|
3380
|
+
}
|
|
3381
|
+
function inferVendor(entry) {
|
|
3382
|
+
const hay = `${entry.id} ${entry.name ?? ""} ${(entry.authors ?? []).join(" ")}`.toLowerCase();
|
|
3383
|
+
if (hay.includes("anthropic") || hay.includes("claude")) return "anthropic";
|
|
3384
|
+
if (hay.includes("google") || hay.includes("gemini")) return "google";
|
|
3385
|
+
if (hay.includes("openai") || hay.includes("codex")) return "openai";
|
|
3386
|
+
if (hay.includes("github") || hay.includes("copilot")) return "github";
|
|
3387
|
+
return "community";
|
|
3388
|
+
}
|
|
3389
|
+
async function fetchAcpRegistry(opts = {}) {
|
|
3390
|
+
const url = opts.url ?? ACP_REGISTRY_URL;
|
|
3391
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
3392
|
+
const controller = new AbortController();
|
|
3393
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3394
|
+
const onParentAbort = () => controller.abort();
|
|
3395
|
+
if (opts.signal) {
|
|
3396
|
+
if (opts.signal.aborted) controller.abort();
|
|
3397
|
+
else opts.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
3398
|
+
}
|
|
3399
|
+
try {
|
|
3400
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
3401
|
+
if (!res.ok) {
|
|
3402
|
+
throw new Error(`ACP registry fetch failed: HTTP ${res.status}`);
|
|
3403
|
+
}
|
|
3404
|
+
const body = await res.json();
|
|
3405
|
+
const rawAgents = Array.isArray(body) ? body : Array.isArray(body?.agents) ? body.agents : null;
|
|
3406
|
+
if (!rawAgents) {
|
|
3407
|
+
throw new Error("ACP registry response had no agents array");
|
|
3408
|
+
}
|
|
3409
|
+
const platformKey = opts.platformKey ?? currentPlatformKey();
|
|
3410
|
+
const agents = [];
|
|
3411
|
+
for (const raw of rawAgents) {
|
|
3412
|
+
const mapped = mapRegistryEntry(raw, platformKey);
|
|
3413
|
+
if (mapped) agents.push(mapped);
|
|
3414
|
+
}
|
|
3415
|
+
return { fetchedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(), agents };
|
|
3416
|
+
} finally {
|
|
3417
|
+
clearTimeout(timer);
|
|
3418
|
+
opts.signal?.removeEventListener("abort", onParentAbort);
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
var DEFAULT_MAX_CONCURRENCY = 4;
|
|
3422
|
+
async function mapBound(items, worker, limit) {
|
|
3423
|
+
const results = new Array(items.length);
|
|
3424
|
+
if (items.length === 0) return results;
|
|
3425
|
+
const safeLimit = Math.max(1, Math.min(limit, items.length));
|
|
3426
|
+
let nextIndex = 0;
|
|
3427
|
+
const workerCount = Math.min(safeLimit, items.length);
|
|
3428
|
+
const runners = [];
|
|
3429
|
+
for (let w = 0; w < workerCount; w++) {
|
|
3430
|
+
runners.push(
|
|
3431
|
+
(async () => {
|
|
3432
|
+
while (true) {
|
|
3433
|
+
const current = nextIndex++;
|
|
3434
|
+
if (current >= items.length) return;
|
|
3435
|
+
results[current] = await worker(items[current], current);
|
|
3436
|
+
}
|
|
3437
|
+
})()
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
await Promise.all(runners);
|
|
3441
|
+
return results;
|
|
3442
|
+
}
|
|
3443
|
+
var defaultEnsembleCmdResolver = (id) => resolveAcpAgentCommand(id);
|
|
2061
3444
|
function setResult(results, agentId, patch) {
|
|
2062
3445
|
const i = results.findIndex((r) => r.agentId === agentId);
|
|
2063
3446
|
if (i < 0) return;
|
|
2064
3447
|
const current = results[i];
|
|
2065
3448
|
results[i] = { ...current, ...patch };
|
|
2066
3449
|
}
|
|
2067
|
-
async function runOne(agentId, cmd, task, timeoutMs, signal) {
|
|
3450
|
+
async function runOne(agentId, cmd, task, timeoutMs, signal, onProgress) {
|
|
2068
3451
|
const startedAt = Date.now();
|
|
2069
3452
|
try {
|
|
2070
3453
|
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
2071
3454
|
...cmd,
|
|
2072
|
-
timeoutMs
|
|
3455
|
+
timeoutMs,
|
|
3456
|
+
...onProgress ? { onProgress: (event) => onProgress(agentId, event) } : {}
|
|
2073
3457
|
});
|
|
2074
3458
|
try {
|
|
2075
3459
|
const budget = new SubagentBudget({
|
|
@@ -2173,8 +3557,13 @@ async function runEnsemble(opts) {
|
|
|
2173
3557
|
}
|
|
2174
3558
|
runnable.push({ id, cmd });
|
|
2175
3559
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
3560
|
+
const concurrency = Math.max(
|
|
3561
|
+
1,
|
|
3562
|
+
opts.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY
|
|
3563
|
+
);
|
|
3564
|
+
await mapBound(
|
|
3565
|
+
runnable,
|
|
3566
|
+
async ({ id, cmd }) => {
|
|
2178
3567
|
if (opts.signal?.aborted) {
|
|
2179
3568
|
setResult(results, id, {
|
|
2180
3569
|
status: "cancelled",
|
|
@@ -2183,9 +3572,10 @@ async function runEnsemble(opts) {
|
|
|
2183
3572
|
});
|
|
2184
3573
|
return;
|
|
2185
3574
|
}
|
|
2186
|
-
const outcome = await runOne(id, cmd, opts.task, timeoutMs, opts.signal);
|
|
3575
|
+
const outcome = await runOne(id, cmd, opts.task, timeoutMs, opts.signal, opts.onProgress);
|
|
2187
3576
|
setResult(results, id, outcome);
|
|
2188
|
-
}
|
|
3577
|
+
},
|
|
3578
|
+
concurrency
|
|
2189
3579
|
);
|
|
2190
3580
|
const summary = { succeeded: 0, failed: 0, skipped: 0, cancelled: 0 };
|
|
2191
3581
|
for (const r of results) {
|
|
@@ -2242,7 +3632,212 @@ Ensemble summary: ${succeeded} succeeded, ${failed} failed, ${cancelled} cancell
|
|
|
2242
3632
|
);
|
|
2243
3633
|
return lines.join("\n");
|
|
2244
3634
|
}
|
|
3635
|
+
function firstLine(s) {
|
|
3636
|
+
const line = s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
|
|
3637
|
+
return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
|
|
3638
|
+
}
|
|
3639
|
+
function randomMarker() {
|
|
3640
|
+
return `ACP_OK_${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
|
|
3641
|
+
}
|
|
3642
|
+
async function benchOne(agentId, cmd, opts) {
|
|
3643
|
+
const checks = [];
|
|
3644
|
+
const startedAt = opts.now();
|
|
3645
|
+
let session = null;
|
|
3646
|
+
const signal = opts.signal ?? new AbortController().signal;
|
|
3647
|
+
const hsStart = opts.now();
|
|
3648
|
+
try {
|
|
3649
|
+
session = await ACPSession.start({
|
|
3650
|
+
command: cmd.command,
|
|
3651
|
+
...cmd.args !== void 0 ? { args: [...cmd.args] } : {},
|
|
3652
|
+
...cmd.env !== void 0 ? { env: cmd.env } : {},
|
|
3653
|
+
projectRoot: opts.projectRoot,
|
|
3654
|
+
timeoutMs: opts.timeoutMs
|
|
3655
|
+
});
|
|
3656
|
+
} catch (err) {
|
|
3657
|
+
const reason2 = err instanceof Error ? err.message : String(err);
|
|
3658
|
+
checks.push({ name: "handshake", ok: false, detail: reason2 });
|
|
3659
|
+
return {
|
|
3660
|
+
agentId,
|
|
3661
|
+
status: "fail",
|
|
3662
|
+
checks,
|
|
3663
|
+
reason: reason2,
|
|
3664
|
+
handshakeMs: opts.now() - hsStart,
|
|
3665
|
+
durationMs: opts.now() - startedAt
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
const handshakeMs = opts.now() - hsStart;
|
|
3669
|
+
const agentInfo = session.getAgentInfo() ?? void 0;
|
|
3670
|
+
checks.push({
|
|
3671
|
+
name: "handshake",
|
|
3672
|
+
ok: true,
|
|
3673
|
+
detail: agentInfo ? `${agentInfo.name} ${agentInfo.version}` : void 0
|
|
3674
|
+
});
|
|
3675
|
+
let promptMs;
|
|
3676
|
+
let sample;
|
|
3677
|
+
let reason;
|
|
3678
|
+
try {
|
|
3679
|
+
const pStart = opts.now();
|
|
3680
|
+
const res = await session.prompt(
|
|
3681
|
+
[textContent(`Reply with exactly this token and nothing else: ${opts.marker}`)],
|
|
3682
|
+
signal
|
|
3683
|
+
);
|
|
3684
|
+
promptMs = opts.now() - pStart;
|
|
3685
|
+
sample = res.text ? firstLine(res.text) : void 0;
|
|
3686
|
+
const promptOk = res.hasText && res.stopReason !== "refusal";
|
|
3687
|
+
checks.push({
|
|
3688
|
+
name: "prompt",
|
|
3689
|
+
ok: promptOk,
|
|
3690
|
+
detail: `stopReason=${res.stopReason}${res.hasText ? "" : ", no text"}`
|
|
3691
|
+
});
|
|
3692
|
+
const markerOk = res.text.includes(opts.marker);
|
|
3693
|
+
checks.push({
|
|
3694
|
+
name: "marker",
|
|
3695
|
+
ok: markerOk,
|
|
3696
|
+
detail: markerOk ? void 0 : "reply did not contain the token"
|
|
3697
|
+
});
|
|
3698
|
+
if (opts.checkFs) {
|
|
3699
|
+
const fileToken = `FILE_${opts.marker}`;
|
|
3700
|
+
const fileName = `acp-bench-${opts.marker}.txt`;
|
|
3701
|
+
const filePath = path.join(opts.projectRoot, fileName);
|
|
3702
|
+
let fsOk = false;
|
|
3703
|
+
let fsDetail;
|
|
3704
|
+
try {
|
|
3705
|
+
await fsp.writeFile(filePath, fileToken, "utf8");
|
|
3706
|
+
const fsRes = await session.prompt(
|
|
3707
|
+
[
|
|
3708
|
+
textContent(
|
|
3709
|
+
`Read the file "${fileName}" in the current directory and reply with its exact contents.`
|
|
3710
|
+
)
|
|
3711
|
+
],
|
|
3712
|
+
signal
|
|
3713
|
+
);
|
|
3714
|
+
fsOk = fsRes.text.includes(fileToken);
|
|
3715
|
+
if (!fsOk) fsDetail = "agent did not return the file contents (may not have used a read tool)";
|
|
3716
|
+
} catch (err) {
|
|
3717
|
+
fsDetail = err instanceof Error ? err.message : String(err);
|
|
3718
|
+
} finally {
|
|
3719
|
+
await fsp.rm(filePath, { force: true }).catch(() => {
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
checks.push({ name: "fs", ok: fsOk, detail: fsDetail });
|
|
3723
|
+
}
|
|
3724
|
+
} catch (err) {
|
|
3725
|
+
reason = err instanceof Error ? err.message : String(err);
|
|
3726
|
+
checks.push({ name: "prompt", ok: false, detail: reason });
|
|
3727
|
+
} finally {
|
|
3728
|
+
try {
|
|
3729
|
+
await session.close();
|
|
3730
|
+
} catch {
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
const required = checks.filter((c) => c.name !== "fs" || opts.checkFs);
|
|
3734
|
+
const allReq = required.every((c) => c.ok);
|
|
3735
|
+
const handshakeOk = checks.find((c) => c.name === "handshake")?.ok === true;
|
|
3736
|
+
const status = allReq ? "pass" : handshakeOk ? "partial" : "fail";
|
|
3737
|
+
return {
|
|
3738
|
+
agentId,
|
|
3739
|
+
status,
|
|
3740
|
+
checks,
|
|
3741
|
+
...agentInfo ? { agentInfo } : {},
|
|
3742
|
+
handshakeMs,
|
|
3743
|
+
...promptMs !== void 0 ? { promptMs } : {},
|
|
3744
|
+
...sample ? { sample } : {},
|
|
3745
|
+
...reason ? { reason } : {},
|
|
3746
|
+
durationMs: opts.now() - startedAt
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3749
|
+
async function runAcpBench(opts) {
|
|
3750
|
+
const now = opts.now ?? Date.now;
|
|
3751
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
3752
|
+
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
3753
|
+
const checkFs = opts.checkFs ?? false;
|
|
3754
|
+
const marker = opts.marker ?? randomMarker();
|
|
3755
|
+
const concurrency = Math.max(1, opts.concurrency ?? 2);
|
|
3756
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3757
|
+
const ids = [];
|
|
3758
|
+
for (const raw of opts.agentIds) {
|
|
3759
|
+
const id = raw.trim();
|
|
3760
|
+
if (id && !seen.has(id)) {
|
|
3761
|
+
seen.add(id);
|
|
3762
|
+
ids.push(id);
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
const results = ids.map((agentId) => ({
|
|
3766
|
+
agentId,
|
|
3767
|
+
status: "skipped",
|
|
3768
|
+
checks: [],
|
|
3769
|
+
durationMs: 0,
|
|
3770
|
+
reason: "unknown agent"
|
|
3771
|
+
}));
|
|
3772
|
+
const startMs = now();
|
|
3773
|
+
const runnable = [];
|
|
3774
|
+
ids.forEach((id, index) => {
|
|
3775
|
+
const cmd = opts.resolveCmd(id);
|
|
3776
|
+
if (cmd) runnable.push({ id, cmd, index });
|
|
3777
|
+
});
|
|
3778
|
+
let next = 0;
|
|
3779
|
+
const workers = [];
|
|
3780
|
+
const workerCount = Math.min(concurrency, runnable.length);
|
|
3781
|
+
for (let w = 0; w < workerCount; w++) {
|
|
3782
|
+
workers.push(
|
|
3783
|
+
(async () => {
|
|
3784
|
+
while (true) {
|
|
3785
|
+
const current = next++;
|
|
3786
|
+
if (current >= runnable.length) return;
|
|
3787
|
+
const { id, cmd, index } = runnable[current];
|
|
3788
|
+
if (opts.signal?.aborted) {
|
|
3789
|
+
results[index] = {
|
|
3790
|
+
agentId: id,
|
|
3791
|
+
status: "skipped",
|
|
3792
|
+
checks: [],
|
|
3793
|
+
durationMs: 0,
|
|
3794
|
+
reason: "aborted"
|
|
3795
|
+
};
|
|
3796
|
+
continue;
|
|
3797
|
+
}
|
|
3798
|
+
opts.onProgress?.(id, "start");
|
|
3799
|
+
const r = await benchOne(id, cmd, {
|
|
3800
|
+
projectRoot,
|
|
3801
|
+
timeoutMs,
|
|
3802
|
+
checkFs,
|
|
3803
|
+
marker,
|
|
3804
|
+
now,
|
|
3805
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
3806
|
+
});
|
|
3807
|
+
results[index] = r;
|
|
3808
|
+
opts.onProgress?.(id, "done", r);
|
|
3809
|
+
}
|
|
3810
|
+
})()
|
|
3811
|
+
);
|
|
3812
|
+
}
|
|
3813
|
+
await Promise.all(workers);
|
|
3814
|
+
const summary = { pass: 0, partial: 0, fail: 0, skipped: 0 };
|
|
3815
|
+
for (const r of results) summary[r.status]++;
|
|
3816
|
+
return { results, summary, totalDurationMs: now() - startMs };
|
|
3817
|
+
}
|
|
3818
|
+
function renderAcpBenchText(result) {
|
|
3819
|
+
const icon = (s) => s === "pass" ? "\u2713" : s === "partial" ? "\u25D0" : s === "skipped" ? "\u2013" : "\u2717";
|
|
3820
|
+
const lines = ["ACP client bench:", ""];
|
|
3821
|
+
if (result.results.length === 0) {
|
|
3822
|
+
lines.push("No agents to bench.");
|
|
3823
|
+
return lines.join("\n");
|
|
3824
|
+
}
|
|
3825
|
+
for (const r of result.results) {
|
|
3826
|
+
const checks = r.checks.map((c) => `${c.ok ? "\u2713" : "\u2717"}${c.name}`).join(" ");
|
|
3827
|
+
const timing = r.handshakeMs !== void 0 ? ` hs=${r.handshakeMs}ms${r.promptMs !== void 0 ? ` prompt=${r.promptMs}ms` : ""}` : "";
|
|
3828
|
+
lines.push(` ${icon(r.status)} ${r.agentId.padEnd(16)} ${r.status.toUpperCase().padEnd(7)} ${checks}${timing}`);
|
|
3829
|
+
if (r.agentInfo) lines.push(` agent: ${r.agentInfo.name} ${r.agentInfo.version}`);
|
|
3830
|
+
if (r.sample) lines.push(` reply: ${r.sample}`);
|
|
3831
|
+
if (r.reason) lines.push(` reason: ${r.reason}`);
|
|
3832
|
+
}
|
|
3833
|
+
const { pass, partial, fail, skipped } = result.summary;
|
|
3834
|
+
lines.push("");
|
|
3835
|
+
lines.push(
|
|
3836
|
+
`Bench summary: ${pass} pass, ${partial} partial, ${fail} fail, ${skipped} skipped. (${result.totalDurationMs}ms total)`
|
|
3837
|
+
);
|
|
3838
|
+
return lines.join("\n");
|
|
3839
|
+
}
|
|
2245
3840
|
|
|
2246
|
-
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 };
|
|
3841
|
+
export { ACPProtocolHandler, ACPSession, ACPSessionError, ACPToolsRegistry, ACP_AGENT_COMMANDS, ACP_REGISTRY_URL, AGENTS_CATALOG, ClientTransport, EnsembleRegistry, FileServer, FsError, REGISTRY_ID_ALIASES, StdioTransport, TerminalServer, ToolTranslator, WebSocketClientTransport, WrongStackACPServer, audioContent, currentPlatformKey, defaultEnsembleCmdResolver, defaultPermissionPolicy, fetchAcpRegistry, findAgentDescriptor, imageContent, makeACPSubagentRunner, makeACPSubagentRunnerWithStop, makePermissionPolicy, mapRegistryEntry, probeAcpAgent, probeAcpAgents, readOnlyPermissionPolicy, renderAcpBenchText, renderEnsembleText, resolveAcpAgentCommand, runAcpBench, runEnsemble, runOneAcpTask, textContent };
|
|
2247
3842
|
//# sourceMappingURL=index.js.map
|
|
2248
3843
|
//# sourceMappingURL=index.js.map
|