@wrongstack/acp 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/protocol-contract.d.ts +22 -2
- package/dist/agent/protocol-handler.d.ts +1 -1
- package/dist/agent/protocol-session-ops.d.ts +12 -1
- package/dist/agent/server-agent-turn.d.ts +6 -2
- package/dist/agent.js +122 -16
- package/dist/client/acp-session-errors.d.ts +6 -0
- package/dist/client/acp-session.d.ts +12 -0
- package/dist/client.js +90 -4
- package/dist/index.js +221 -35
- package/dist/integration/acp-subagent-runner.d.ts +8 -2
- package/dist/registry/acp-registry-fetch.d.ts +2 -0
- package/dist/types/acp-v1.d.ts +9 -1
- package/dist/wrongstack-acp-agent.js +116 -15
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -49,6 +49,64 @@ var DEFAULT_MODES = [
|
|
|
49
49
|
description: "Default agent mode for code-generation tasks."
|
|
50
50
|
}
|
|
51
51
|
];
|
|
52
|
+
function parseMcpServers(raw, onSkipped) {
|
|
53
|
+
if (!Array.isArray(raw)) return [];
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const entry of raw) {
|
|
56
|
+
if (typeof entry !== "object" || entry === null) {
|
|
57
|
+
onSkipped?.("entry is not an object");
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const e = entry;
|
|
61
|
+
const name = typeof e.name === "string" ? e.name.trim() : "";
|
|
62
|
+
if (name === "") {
|
|
63
|
+
onSkipped?.("entry has no name");
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const type = typeof e.type === "string" ? e.type : "stdio";
|
|
67
|
+
if (type === "http" || type === "sse") {
|
|
68
|
+
if (typeof e.url !== "string" || e.url === "") {
|
|
69
|
+
onSkipped?.(`"${name}": ${type} server has no url`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const headers = parseNameValuePairs(e.headers);
|
|
73
|
+
const url = e.url;
|
|
74
|
+
out.push(
|
|
75
|
+
type === "http" ? { type: "http", name, url, ...headers ? { headers } : {} } : { type: "sse", name, url, ...headers ? { headers } : {} }
|
|
76
|
+
);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (type !== "stdio") {
|
|
80
|
+
onSkipped?.(`"${name}": unknown transport "${type}"`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeof e.command !== "string" || e.command === "") {
|
|
84
|
+
onSkipped?.(`"${name}": stdio server has no command`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
|
|
88
|
+
const env = parseNameValuePairs(e.env);
|
|
89
|
+
out.push({
|
|
90
|
+
name,
|
|
91
|
+
command: e.command,
|
|
92
|
+
...args && args.length > 0 ? { args } : {},
|
|
93
|
+
...env ? { env } : {}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function parseNameValuePairs(raw) {
|
|
99
|
+
if (!Array.isArray(raw)) return void 0;
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const pair of raw) {
|
|
102
|
+
if (typeof pair !== "object" || pair === null) continue;
|
|
103
|
+
const p = pair;
|
|
104
|
+
if (typeof p.name === "string" && p.name !== "" && typeof p.value === "string") {
|
|
105
|
+
out.push({ name: p.name, value: p.value });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out.length > 0 ? out : void 0;
|
|
109
|
+
}
|
|
52
110
|
async function resolveSessionCwd(requested) {
|
|
53
111
|
if (!path.isAbsolute(requested)) return null;
|
|
54
112
|
const resolved = path.resolve(requested);
|
|
@@ -131,9 +189,16 @@ function buildInitializeResult(agentName, modes, configOptions) {
|
|
|
131
189
|
audio: false,
|
|
132
190
|
embeddedContext: true
|
|
133
191
|
},
|
|
192
|
+
// All three ACP transports are supported. stdio is mandatory per spec
|
|
193
|
+
// and cannot be declined; http and sse are declared here because the
|
|
194
|
+
// agent now actually connects them (see `parseMcpServers` above and the
|
|
195
|
+
// per-session MCP registry in `buildAcpServerAgentFactory`). Before that
|
|
196
|
+
// wiring existed the array was destructured and thrown away at every
|
|
197
|
+
// entry point, so a client got a successful `session/new` and no tools —
|
|
198
|
+
// flip these back to false if that connection path is ever removed.
|
|
134
199
|
mcpCapabilities: {
|
|
135
|
-
http:
|
|
136
|
-
sse:
|
|
200
|
+
http: true,
|
|
201
|
+
sse: true
|
|
137
202
|
},
|
|
138
203
|
sessionCapabilities: {
|
|
139
204
|
close: {},
|
|
@@ -173,6 +238,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
173
238
|
}
|
|
174
239
|
cwd = resolved;
|
|
175
240
|
}
|
|
241
|
+
const skipped = [];
|
|
242
|
+
const mcpServers = parseMcpServers(p.mcpServers, (reason) => skipped.push(reason));
|
|
176
243
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
177
244
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
178
245
|
const state = {
|
|
@@ -181,7 +248,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
181
248
|
abort: new AbortController(),
|
|
182
249
|
modeId: DEFAULT_MODE_ID,
|
|
183
250
|
createdAt: now,
|
|
184
|
-
updatedAt: now
|
|
251
|
+
updatedAt: now,
|
|
252
|
+
...mcpServers.length > 0 ? { mcpServers } : {}
|
|
185
253
|
};
|
|
186
254
|
ctx.sessions.set(sessionId, state);
|
|
187
255
|
ctx.onSessionNew(state);
|
|
@@ -202,6 +270,7 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
202
270
|
}
|
|
203
271
|
});
|
|
204
272
|
}
|
|
273
|
+
await reportSkippedMcpServers(ctx, sessionId, skipped);
|
|
205
274
|
await ctx.sendResult(id, {
|
|
206
275
|
sessionId,
|
|
207
276
|
modes: ctx.modes,
|
|
@@ -213,6 +282,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
213
282
|
const p = params ?? {};
|
|
214
283
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
215
284
|
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
285
|
+
const loadSkipped = [];
|
|
286
|
+
const loadMcpServers = parseMcpServers(p.mcpServers, (reason) => loadSkipped.push(reason));
|
|
216
287
|
const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
217
288
|
if (!existing && sessionId && ctx.store) {
|
|
218
289
|
const persisted = await ctx.store.load(sessionId);
|
|
@@ -234,7 +305,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
234
305
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
235
306
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
236
307
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
237
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
308
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {},
|
|
309
|
+
...loadMcpServers.length > 0 ? { mcpServers: loadMcpServers } : {}
|
|
238
310
|
};
|
|
239
311
|
ctx.sessions.set(sessionId, restored);
|
|
240
312
|
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
@@ -245,6 +317,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
245
317
|
sessionId,
|
|
246
318
|
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
247
319
|
});
|
|
320
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
248
321
|
await ctx.sendResult(id, {
|
|
249
322
|
initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
|
|
250
323
|
});
|
|
@@ -253,6 +326,9 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
253
326
|
}
|
|
254
327
|
if (existing) {
|
|
255
328
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
329
|
+
if (loadMcpServers.length > 0) {
|
|
330
|
+
existing.mcpServers = loadMcpServers;
|
|
331
|
+
}
|
|
256
332
|
const replay = ctx.replayFor?.(sessionId);
|
|
257
333
|
if (replay) {
|
|
258
334
|
for (const update of replay) {
|
|
@@ -273,6 +349,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
273
349
|
modeId: existing.modeId
|
|
274
350
|
}
|
|
275
351
|
});
|
|
352
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
276
353
|
await ctx.sendResult(id, {
|
|
277
354
|
initialMode: {
|
|
278
355
|
currentModeId: existing.modeId,
|
|
@@ -305,6 +382,9 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
305
382
|
}
|
|
306
383
|
forkCwd = resolved;
|
|
307
384
|
}
|
|
385
|
+
const forkSkipped = [];
|
|
386
|
+
const forkRequested = parseMcpServers(p.mcpServers, (reason) => forkSkipped.push(reason));
|
|
387
|
+
const forkMcpServers = forkRequested.length > 0 ? forkRequested : source.mcpServers;
|
|
308
388
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
309
389
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
310
390
|
const forked = {
|
|
@@ -314,7 +394,8 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
314
394
|
modeId: source.modeId,
|
|
315
395
|
createdAt: now,
|
|
316
396
|
updatedAt: now,
|
|
317
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
397
|
+
...source.title !== void 0 ? { title: source.title } : {},
|
|
398
|
+
...forkMcpServers && forkMcpServers.length > 0 ? { mcpServers: forkMcpServers } : {}
|
|
318
399
|
};
|
|
319
400
|
const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
320
401
|
sessionUpdate: update.sessionUpdate,
|
|
@@ -328,6 +409,7 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
328
409
|
sessionId,
|
|
329
410
|
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
330
411
|
});
|
|
412
|
+
await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
|
|
331
413
|
await ctx.sendResult(id, {
|
|
332
414
|
sessionId,
|
|
333
415
|
modes: ctx.modes,
|
|
@@ -367,7 +449,13 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
367
449
|
};
|
|
368
450
|
try {
|
|
369
451
|
result = await ctx.runTurn(
|
|
370
|
-
{
|
|
452
|
+
{
|
|
453
|
+
sessionId,
|
|
454
|
+
prompt: p.prompt,
|
|
455
|
+
signal: turnSignal.signal,
|
|
456
|
+
cwd: session.cwd,
|
|
457
|
+
...session.mcpServers ? { mcpServers: session.mcpServers } : {}
|
|
458
|
+
},
|
|
371
459
|
emit,
|
|
372
460
|
api
|
|
373
461
|
);
|
|
@@ -425,6 +513,22 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
425
513
|
await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
|
|
426
514
|
return false;
|
|
427
515
|
}
|
|
516
|
+
async function reportSkippedMcpServers(ctx, sessionId, skipped) {
|
|
517
|
+
if (skipped.length === 0) return;
|
|
518
|
+
try {
|
|
519
|
+
await ctx.sendNotification({
|
|
520
|
+
sessionId,
|
|
521
|
+
update: {
|
|
522
|
+
sessionUpdate: "agent_message_chunk",
|
|
523
|
+
content: {
|
|
524
|
+
type: "text",
|
|
525
|
+
text: `Ignored ${skipped.length} malformed mcpServers entr${skipped.length === 1 ? "y" : "ies"}: ${skipped.join("; ")}`
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
}
|
|
428
532
|
|
|
429
533
|
// src/agent/protocol-handler.ts
|
|
430
534
|
var ACPProtocolHandler = class {
|
|
@@ -623,7 +727,7 @@ var ACPProtocolHandler = class {
|
|
|
623
727
|
return false;
|
|
624
728
|
}
|
|
625
729
|
async handleAuthenticate(id, _params) {
|
|
626
|
-
await this.sendResult(id, {
|
|
730
|
+
await this.sendResult(id, {});
|
|
627
731
|
return false;
|
|
628
732
|
}
|
|
629
733
|
async handleLogout(id, _params) {
|
|
@@ -636,6 +740,10 @@ var ACPProtocolHandler = class {
|
|
|
636
740
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
637
741
|
if (existing) {
|
|
638
742
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
743
|
+
const resumeServers = parseMcpServers(p.mcpServers);
|
|
744
|
+
if (resumeServers.length > 0) {
|
|
745
|
+
existing.mcpServers = resumeServers;
|
|
746
|
+
}
|
|
639
747
|
await this.sendResult(id, {
|
|
640
748
|
initialMode: {
|
|
641
749
|
currentModeId: existing.modeId,
|
|
@@ -1293,11 +1401,11 @@ function toolToPriority(tool) {
|
|
|
1293
1401
|
}
|
|
1294
1402
|
|
|
1295
1403
|
// src/agent/wrongstack-acp-agent.ts
|
|
1296
|
-
import { timingSafeEqual } from "node:crypto";
|
|
1297
1404
|
import { createServer } from "node:http";
|
|
1298
1405
|
import { isIP } from "node:net";
|
|
1299
1406
|
import { fileURLToPath } from "node:url";
|
|
1300
1407
|
import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1408
|
+
import { timingSafeTokenEqual } from "@wrongstack/primitives";
|
|
1301
1409
|
var LISTEN_RETRY_LIMIT = 5;
|
|
1302
1410
|
var LISTEN_RETRY_BASE_MS = 25;
|
|
1303
1411
|
var WrongStackACPServer = class {
|
|
@@ -1531,13 +1639,6 @@ var WrongStackACPServer = class {
|
|
|
1531
1639
|
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
1532
1640
|
return { stopReason: "end_turn" };
|
|
1533
1641
|
};
|
|
1534
|
-
function timingSafeTokenEqual(supplied, expected) {
|
|
1535
|
-
if (!supplied || !expected) return false;
|
|
1536
|
-
const a = Buffer.from(supplied);
|
|
1537
|
-
const b = Buffer.from(expected);
|
|
1538
|
-
if (a.length !== b.length) return false;
|
|
1539
|
-
return timingSafeEqual(a, b);
|
|
1540
|
-
}
|
|
1541
1642
|
function isLoopbackPeer(req) {
|
|
1542
1643
|
const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
|
|
1543
1644
|
return address !== void 0 && isLoopbackHost(address);
|
|
@@ -2045,6 +2146,20 @@ var ACPSessionError = class extends Error {
|
|
|
2045
2146
|
function isJsonRpcError(v) {
|
|
2046
2147
|
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
2047
2148
|
}
|
|
2149
|
+
function isAuthRequiredError(err) {
|
|
2150
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2151
|
+
if (/auth(_|-)?required|authentication required/i.test(message)) return true;
|
|
2152
|
+
const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
|
|
2153
|
+
if (!cause || typeof cause !== "object") return false;
|
|
2154
|
+
const data = cause.data;
|
|
2155
|
+
if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
|
|
2156
|
+
if (data && typeof data === "object") {
|
|
2157
|
+
const d = data;
|
|
2158
|
+
if (d.authRequired === true) return true;
|
|
2159
|
+
if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
|
|
2160
|
+
}
|
|
2161
|
+
return false;
|
|
2162
|
+
}
|
|
2048
2163
|
|
|
2049
2164
|
// src/client/acp-session-ops.ts
|
|
2050
2165
|
function filterMcpServers(agentCapabilities, servers) {
|
|
@@ -3089,7 +3204,7 @@ var ACPSession = class _ACPSession {
|
|
|
3089
3204
|
fs: { readTextFile: true, writeTextFile: true },
|
|
3090
3205
|
terminal: true
|
|
3091
3206
|
},
|
|
3092
|
-
clientInfo: { name: "wrongstack", title: "WrongStack", version:
|
|
3207
|
+
clientInfo: { name: "wrongstack", title: "WrongStack", version: ACP_PACKAGE_VERSION }
|
|
3093
3208
|
});
|
|
3094
3209
|
if (isJsonRpcError(result)) {
|
|
3095
3210
|
throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
|
|
@@ -3121,12 +3236,13 @@ var ACPSession = class _ACPSession {
|
|
|
3121
3236
|
if (this.state === "closed") {
|
|
3122
3237
|
throw new ACPSessionError("closed", "session is closed");
|
|
3123
3238
|
}
|
|
3124
|
-
if (this.state !== "ready") {
|
|
3239
|
+
if (this.state !== "ready" && this.state !== "authenticated") {
|
|
3125
3240
|
throw new ACPSessionError(
|
|
3126
3241
|
"protocol_error",
|
|
3127
3242
|
`authenticate called in state=${this.state} (expected 'ready')`
|
|
3128
3243
|
);
|
|
3129
3244
|
}
|
|
3245
|
+
if (this.state === "authenticated") return;
|
|
3130
3246
|
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
3131
3247
|
throw new ACPSessionError(
|
|
3132
3248
|
"auth_failed",
|
|
@@ -3229,7 +3345,7 @@ var ACPSession = class _ACPSession {
|
|
|
3229
3345
|
return emptyRunResult("cancelled");
|
|
3230
3346
|
}
|
|
3231
3347
|
if (!this.sessionId) {
|
|
3232
|
-
this.sessionId = await
|
|
3348
|
+
this.sessionId = await this.createSessionWithAuth();
|
|
3233
3349
|
}
|
|
3234
3350
|
if (signal.aborted) {
|
|
3235
3351
|
return emptyRunResult("cancelled");
|
|
@@ -3365,6 +3481,54 @@ var ACPSession = class _ACPSession {
|
|
|
3365
3481
|
});
|
|
3366
3482
|
});
|
|
3367
3483
|
}
|
|
3484
|
+
/**
|
|
3485
|
+
* `session/new`, then one authenticate+retry if the agent demands login.
|
|
3486
|
+
* Logged-in CLIs succeed on the first call even when they advertise
|
|
3487
|
+
* `authMethods`; we do not pop OAuth on every spawn.
|
|
3488
|
+
*/
|
|
3489
|
+
async createSessionWithAuth() {
|
|
3490
|
+
try {
|
|
3491
|
+
return await executeCreateSession(this.opContext());
|
|
3492
|
+
} catch (err) {
|
|
3493
|
+
if (this.state === "authenticated" || !isAuthRequiredError(err)) {
|
|
3494
|
+
throw err instanceof ACPSessionError ? err : new ACPSessionError(
|
|
3495
|
+
"session_create_failed",
|
|
3496
|
+
err instanceof Error ? err.message : String(err),
|
|
3497
|
+
err
|
|
3498
|
+
);
|
|
3499
|
+
}
|
|
3500
|
+
await this.ensureAuthenticated();
|
|
3501
|
+
return executeCreateSession(this.opContext());
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Pick a non-terminal auth method and run `authenticate`. Terminal-only
|
|
3506
|
+
* agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
|
|
3507
|
+
* we refuse rather than hang a TUI inside the JSON-RPC child.
|
|
3508
|
+
*/
|
|
3509
|
+
async ensureAuthenticated() {
|
|
3510
|
+
if (this.state === "authenticated") return;
|
|
3511
|
+
if (this.authMethods.length === 0) {
|
|
3512
|
+
throw new ACPSessionError(
|
|
3513
|
+
"auth_failed",
|
|
3514
|
+
"This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
const inProcess = this.authMethods.find(
|
|
3518
|
+
(m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
|
|
3519
|
+
);
|
|
3520
|
+
if (inProcess) {
|
|
3521
|
+
await this.authenticate(inProcess.id);
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
const terminal = this.authMethods.find((m) => m.type === "terminal");
|
|
3525
|
+
const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
|
|
3526
|
+
const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
|
|
3527
|
+
throw new ACPSessionError(
|
|
3528
|
+
"auth_failed",
|
|
3529
|
+
`This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
|
|
3530
|
+
);
|
|
3531
|
+
}
|
|
3368
3532
|
sendResult(id, result) {
|
|
3369
3533
|
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
3370
3534
|
}
|
|
@@ -3393,7 +3557,11 @@ var ACPSession = class _ACPSession {
|
|
|
3393
3557
|
clearTimeout(pending.timeoutHandle);
|
|
3394
3558
|
this.pending.delete(msg.id);
|
|
3395
3559
|
if (msg.error !== void 0) {
|
|
3396
|
-
|
|
3560
|
+
const method = pending.method;
|
|
3561
|
+
const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
|
|
3562
|
+
pending.reject(
|
|
3563
|
+
new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
|
|
3564
|
+
);
|
|
3397
3565
|
} else {
|
|
3398
3566
|
pending.resolve(msg.result);
|
|
3399
3567
|
}
|
|
@@ -3833,7 +4001,7 @@ var AGENTS_CATALOG = [
|
|
|
3833
4001
|
id: "cline",
|
|
3834
4002
|
displayName: "Cline",
|
|
3835
4003
|
vendor: "community",
|
|
3836
|
-
probe: { command: "
|
|
4004
|
+
probe: { command: "cline", args: ["--version"] },
|
|
3837
4005
|
// Registry id `cline`: the `cline` npm package speaks ACP behind `--acp`.
|
|
3838
4006
|
acp: {
|
|
3839
4007
|
command: "npx",
|
|
@@ -3876,7 +4044,8 @@ var AGENTS_CATALOG = [
|
|
|
3876
4044
|
fs: true
|
|
3877
4045
|
},
|
|
3878
4046
|
integration: "experimental",
|
|
3879
|
-
//
|
|
4047
|
+
// Not in the official agentclientprotocol/registry (2026-09). Keep as a
|
|
4048
|
+
// local-PATH fallback; probe/spawn may hang if the binary has no ACP entry.
|
|
3880
4049
|
docs: "https://github.com/OpenHands/OpenHands"
|
|
3881
4050
|
},
|
|
3882
4051
|
// ── Vendor CLIs (native binaries) ───────────────────────────────────
|
|
@@ -3909,6 +4078,7 @@ var AGENTS_CATALOG = [
|
|
|
3909
4078
|
fs: true
|
|
3910
4079
|
},
|
|
3911
4080
|
integration: "experimental",
|
|
4081
|
+
// Not in the official agentclientprotocol/registry (2026-09).
|
|
3912
4082
|
docs: "https://kiro.dev"
|
|
3913
4083
|
},
|
|
3914
4084
|
{
|
|
@@ -3931,8 +4101,9 @@ var AGENTS_CATALOG = [
|
|
|
3931
4101
|
id: "mistral-vibe",
|
|
3932
4102
|
displayName: "Mistral Vibe",
|
|
3933
4103
|
vendor: "community",
|
|
3934
|
-
probe: { command: "vibe", args: ["--version"] },
|
|
3935
|
-
|
|
4104
|
+
probe: { command: "vibe-acp", args: ["--version"] },
|
|
4105
|
+
// Official registry ships a dedicated `vibe-acp` binary, not bare `vibe`.
|
|
4106
|
+
acp: { command: "vibe-acp", args: [] },
|
|
3936
4107
|
supports: {
|
|
3937
4108
|
loadSession: false,
|
|
3938
4109
|
promptImages: false,
|
|
@@ -4054,6 +4225,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4054
4225
|
} catch {
|
|
4055
4226
|
}
|
|
4056
4227
|
options.onProgress?.(event);
|
|
4228
|
+
try {
|
|
4229
|
+
options.publishLive?.(ctx, task, event);
|
|
4230
|
+
} catch {
|
|
4231
|
+
}
|
|
4057
4232
|
};
|
|
4058
4233
|
try {
|
|
4059
4234
|
const result = await session.prompt([textContent(task.description)], ctx.signal, onProgress);
|
|
@@ -4140,10 +4315,13 @@ var REGISTRY_ID_ALIASES = {
|
|
|
4140
4315
|
"gemini-cli": "gemini",
|
|
4141
4316
|
"codex-cli": "codex-acp",
|
|
4142
4317
|
copilot: "github-copilot-cli",
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4318
|
+
kimi: "kimi",
|
|
4319
|
+
cline: "cline",
|
|
4320
|
+
goose: "goose",
|
|
4321
|
+
opencode: "opencode",
|
|
4322
|
+
cursor: "cursor",
|
|
4323
|
+
"qwen-code": "qwen-code",
|
|
4324
|
+
"mistral-vibe": "mistral-vibe"
|
|
4147
4325
|
};
|
|
4148
4326
|
function resolveAcpAgentCommand(id, overrides, live) {
|
|
4149
4327
|
const ov = overrides?.[id];
|
|
@@ -4566,20 +4744,20 @@ async function runEnsemble(opts) {
|
|
|
4566
4744
|
const detectedById = new Map(detected.map((a) => [a.id, a]));
|
|
4567
4745
|
const runnable = [];
|
|
4568
4746
|
for (const id of requested) {
|
|
4747
|
+
const cmd = resolveCmd(id);
|
|
4569
4748
|
const det = detectedById.get(id);
|
|
4570
|
-
|
|
4749
|
+
const pkgLauncher = cmd?.command === "npx" || cmd?.command === "uvx";
|
|
4750
|
+
if (det && !det.installed && !pkgLauncher) {
|
|
4571
4751
|
setResult(results, id, {
|
|
4572
4752
|
status: "skipped",
|
|
4573
|
-
reason: det
|
|
4753
|
+
reason: det.reason ?? "binary not found"
|
|
4574
4754
|
});
|
|
4575
4755
|
continue;
|
|
4576
4756
|
}
|
|
4577
|
-
const cmd = resolveCmd(id);
|
|
4578
4757
|
if (!cmd) {
|
|
4579
4758
|
setResult(results, id, {
|
|
4580
|
-
status: "
|
|
4581
|
-
|
|
4582
|
-
durationMs: 0
|
|
4759
|
+
status: "skipped",
|
|
4760
|
+
reason: det?.reason ?? "not in catalog"
|
|
4583
4761
|
});
|
|
4584
4762
|
continue;
|
|
4585
4763
|
}
|
|
@@ -4715,9 +4893,17 @@ function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
|
|
|
4715
4893
|
const dist = entry.distribution;
|
|
4716
4894
|
let acp = null;
|
|
4717
4895
|
if (dist?.npx?.package) {
|
|
4718
|
-
acp = {
|
|
4896
|
+
acp = {
|
|
4897
|
+
command: "npx",
|
|
4898
|
+
args: ["-y", dist.npx.package, ...dist.npx.args ?? []],
|
|
4899
|
+
...dist.npx.env ? { env: dist.npx.env } : {}
|
|
4900
|
+
};
|
|
4719
4901
|
} else if (dist?.uvx?.package) {
|
|
4720
|
-
acp = {
|
|
4902
|
+
acp = {
|
|
4903
|
+
command: "uvx",
|
|
4904
|
+
args: [dist.uvx.package, ...dist.uvx.args ?? []],
|
|
4905
|
+
...dist.uvx.env ? { env: dist.uvx.env } : {}
|
|
4906
|
+
};
|
|
4721
4907
|
} else if (dist?.binary) {
|
|
4722
4908
|
const target = dist.binary[platformKey];
|
|
4723
4909
|
if (target?.cmd) {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* Connected to the Director / MultiAgentCoordinator via the
|
|
12
12
|
* `SubagentRunner` interface (same shape as `AgentSubagentRunner`).
|
|
13
13
|
*/
|
|
14
|
-
import type { SubagentRunner } from '@wrongstack/core/types';
|
|
15
|
-
import { type ACPProgressHandler, ACPSession } from '../client/acp-session.js';
|
|
14
|
+
import type { SubagentRunContext, SubagentRunner, TaskSpec } from '@wrongstack/core/types';
|
|
15
|
+
import { type ACPProgressEvent, type ACPProgressHandler, ACPSession } from '../client/acp-session.js';
|
|
16
16
|
import type { PermissionPolicy } from '../client/permission.js';
|
|
17
17
|
import type { McpServer } from '../types/acp-v1.js';
|
|
18
18
|
export interface ACPSubagentRunnerOptions {
|
|
@@ -40,6 +40,12 @@ export interface ACPSubagentRunnerOptions {
|
|
|
40
40
|
* stream, instead of waiting for the buffered final result.
|
|
41
41
|
*/
|
|
42
42
|
onProgress?: ACPProgressHandler | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Host live-view hook. Called with the run context so a fleet/TUI/WebUI
|
|
45
|
+
* publisher can attribute tool calls and text to the right subagent.
|
|
46
|
+
* Invoked in addition to `onProgress`.
|
|
47
|
+
*/
|
|
48
|
+
publishLive?: ((ctx: SubagentRunContext, task: TaskSpec, event: ACPProgressEvent) => void) | undefined;
|
|
43
49
|
/**
|
|
44
50
|
* Permission policy for the external agent's `session/request_permission`
|
|
45
51
|
* calls. Defaults to the session's own default. Inject the host's
|
|
@@ -39,10 +39,12 @@ export interface RegistryAgentEntry {
|
|
|
39
39
|
npx?: {
|
|
40
40
|
package: string;
|
|
41
41
|
args?: string[];
|
|
42
|
+
env?: Record<string, string>;
|
|
42
43
|
};
|
|
43
44
|
uvx?: {
|
|
44
45
|
package: string;
|
|
45
46
|
args?: string[];
|
|
47
|
+
env?: Record<string, string>;
|
|
46
48
|
};
|
|
47
49
|
binary?: Record<string, {
|
|
48
50
|
archive?: string;
|
package/dist/types/acp-v1.d.ts
CHANGED
|
@@ -95,7 +95,15 @@ export interface AuthMethod {
|
|
|
95
95
|
id: string;
|
|
96
96
|
name: string;
|
|
97
97
|
description?: string | undefined;
|
|
98
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Registry agents advertise `agent` (OAuth in-process) or `terminal`
|
|
100
|
+
* (separate login CLI). `oauth`/`http` are spec aliases; `env_var` is
|
|
101
|
+
* used by some agents but is not a registry-supported setup path.
|
|
102
|
+
*/
|
|
103
|
+
type?: 'agent' | 'oauth' | 'http' | 'terminal' | 'env_var' | undefined;
|
|
104
|
+
/** Extra argv for `type: 'terminal'` setup (replaces the ACP entry args). */
|
|
105
|
+
args?: string[] | undefined;
|
|
106
|
+
env?: Record<string, string> | undefined;
|
|
99
107
|
}
|
|
100
108
|
export interface AuthenticateRequest {
|
|
101
109
|
methodId: string;
|