@wrongstack/acp 1.1.1 → 1.1.4
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/stdio-transport.d.ts +2 -0
- package/dist/client/acp-request-state.d.ts +4 -0
- package/dist/client/acp-session-ops.d.ts +19 -0
- package/dist/client/acp-session-prompt.d.ts +2 -2
- package/dist/client/acp-session-types.d.ts +67 -1
- package/dist/client/acp-session-updates.d.ts +9 -0
- package/dist/client/acp-session.d.ts +21 -1
- package/dist/client.js +343 -41
- package/dist/index.d.ts +3 -1
- package/dist/index.js +347 -42
- package/dist/integration/acp-agent-failure.d.ts +20 -0
- package/dist/integration/acp-subagent-runner-options.d.ts +88 -0
- package/dist/integration/acp-subagent-runner.d.ts +4 -63
- package/dist/integration/run-one-acp-task.d.ts +3 -0
- package/dist/registry/contracts.d.ts +6 -0
- package/package.json +3 -3
package/dist/client.js
CHANGED
|
@@ -11,6 +11,19 @@ import {
|
|
|
11
11
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
12
12
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
13
13
|
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
14
|
+
var STDERR_TAIL_CHARS = 4096;
|
|
15
|
+
function echoChildStderr() {
|
|
16
|
+
const v = process.env.WRONGSTACK_DEBUG;
|
|
17
|
+
return !!v && v !== "0" && v !== "false";
|
|
18
|
+
}
|
|
19
|
+
function lastLine(text) {
|
|
20
|
+
const lines = text.split(/\r?\n/);
|
|
21
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
22
|
+
const line = lines[i]?.trim();
|
|
23
|
+
if (line) return line.length > 300 ? `${line.slice(0, 300)}\u2026` : line;
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
14
27
|
function takeLines(pending, chunk) {
|
|
15
28
|
const lines = [];
|
|
16
29
|
let start = 0;
|
|
@@ -27,6 +40,8 @@ function positiveLimit(value, fallback) {
|
|
|
27
40
|
var ClientTransport = class {
|
|
28
41
|
child = null;
|
|
29
42
|
buffer = "";
|
|
43
|
+
stderrTail = "";
|
|
44
|
+
stopRequested = false;
|
|
30
45
|
handlers = /* @__PURE__ */ new Set();
|
|
31
46
|
claimHandlers = /* @__PURE__ */ new Set();
|
|
32
47
|
closeHandlers = /* @__PURE__ */ new Set();
|
|
@@ -127,9 +142,11 @@ var ClientTransport = class {
|
|
|
127
142
|
}
|
|
128
143
|
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
129
144
|
if (idx !== -1) {
|
|
130
|
-
|
|
145
|
+
const rest = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
146
|
+
this.buffer = "";
|
|
131
147
|
child.stdout.removeListener("data", waitForMarker);
|
|
132
148
|
onReady();
|
|
149
|
+
if (rest) this.onChildData(rest);
|
|
133
150
|
}
|
|
134
151
|
};
|
|
135
152
|
child.stdout.on("data", waitForMarker);
|
|
@@ -192,6 +209,7 @@ var ClientTransport = class {
|
|
|
192
209
|
}
|
|
193
210
|
stop(reason = "transport stopped") {
|
|
194
211
|
this.closed = true;
|
|
212
|
+
this.stopRequested = true;
|
|
195
213
|
this.resolveRead?.(null);
|
|
196
214
|
this.resolveRead = null;
|
|
197
215
|
this.buffer = "";
|
|
@@ -229,7 +247,8 @@ var ClientTransport = class {
|
|
|
229
247
|
}
|
|
230
248
|
}
|
|
231
249
|
onChildError(chunk) {
|
|
232
|
-
|
|
250
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_CHARS);
|
|
251
|
+
if (echoChildStderr()) writeErr(`[acp-child stderr] ${chunk}`);
|
|
233
252
|
}
|
|
234
253
|
onChildClose(code) {
|
|
235
254
|
this.closed = true;
|
|
@@ -241,11 +260,16 @@ var ClientTransport = class {
|
|
|
241
260
|
this.queuedChars = 0;
|
|
242
261
|
this.handlers.clear();
|
|
243
262
|
this.claimHandlers.clear();
|
|
244
|
-
|
|
263
|
+
const failed = code !== 0 && code !== null && !this.stopRequested;
|
|
264
|
+
if (failed) {
|
|
265
|
+
const tail = echoChildStderr() ? "" : this.stderrTail.trimEnd();
|
|
245
266
|
writeErr(`[acp-child exited with code ${code}]
|
|
246
|
-
|
|
267
|
+
${tail ? `${tail}
|
|
268
|
+
` : ""}`);
|
|
247
269
|
}
|
|
248
|
-
|
|
270
|
+
const cause = failed ? lastLine(this.stderrTail) : "";
|
|
271
|
+
this.stderrTail = "";
|
|
272
|
+
this.notifyClosed(`agent process exited with code ${code}${cause ? `: ${cause}` : ""}`);
|
|
249
273
|
}
|
|
250
274
|
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
251
275
|
if (this.resolveRead) {
|
|
@@ -388,6 +412,9 @@ function isAuthRequiredError(err) {
|
|
|
388
412
|
}
|
|
389
413
|
|
|
390
414
|
// src/client/acp-request-state.ts
|
|
415
|
+
function touchIdleRequests(pending) {
|
|
416
|
+
for (const p of pending.values()) p.rearm?.();
|
|
417
|
+
}
|
|
391
418
|
function rejectPendingRequests(pending, reason) {
|
|
392
419
|
for (const p of pending.values()) {
|
|
393
420
|
clearTimeout(p.timeoutHandle);
|
|
@@ -584,11 +611,21 @@ async function executeMcpMessage(ctx, connectionId, message) {
|
|
|
584
611
|
}
|
|
585
612
|
async function executeCreateSession(ctx) {
|
|
586
613
|
const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
|
|
587
|
-
const
|
|
588
|
-
const result = await ctx.sendRequest(id, "session/new", {
|
|
614
|
+
const params = {
|
|
589
615
|
cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
590
|
-
mcpServers: servers
|
|
591
|
-
|
|
616
|
+
mcpServers: servers,
|
|
617
|
+
...ctx.opts.sessionMeta ? { _meta: ctx.opts.sessionMeta } : {}
|
|
618
|
+
};
|
|
619
|
+
const resumeId = ctx.opts.resumeSessionId;
|
|
620
|
+
if (resumeId && ctx.agentCapabilities.sessionCapabilities?.resume) {
|
|
621
|
+
const resumed = await ctx.sendRequest(ctx.allocId(), "session/resume", { ...params, sessionId: resumeId }).catch((err) => ({ refused: err }));
|
|
622
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
623
|
+
if (!isJsonRpcError(resumed) && !resumed.refused) {
|
|
624
|
+
return adoptSessionMode(ctx, resumeId, resumed);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const id = ctx.allocId();
|
|
628
|
+
const result = await ctx.sendRequest(id, "session/new", params);
|
|
592
629
|
if (isJsonRpcError(result)) {
|
|
593
630
|
throw new ACPSessionError(
|
|
594
631
|
"session_create_failed",
|
|
@@ -600,8 +637,53 @@ async function executeCreateSession(ctx) {
|
|
|
600
637
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
601
638
|
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
602
639
|
}
|
|
640
|
+
return adoptSessionMode(ctx, sessionId, result);
|
|
641
|
+
}
|
|
642
|
+
async function executeOpenSession(ctx, create) {
|
|
643
|
+
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
644
|
+
if (ctx.sessionId) return { sessionId: ctx.sessionId, resumed: false };
|
|
645
|
+
const sessionId = await create();
|
|
646
|
+
ctx.setSessionId(sessionId);
|
|
647
|
+
return { sessionId, resumed: sessionId === ctx.opts.resumeSessionId };
|
|
648
|
+
}
|
|
649
|
+
async function adoptSessionMode(ctx, sessionId, result) {
|
|
650
|
+
const want = ctx.opts.initialModeId;
|
|
651
|
+
const modes = result.modes;
|
|
652
|
+
const offered = Array.isArray(modes?.availableModes) && modes.availableModes.some((m) => m?.id === want);
|
|
653
|
+
if (want && !offered && ctx.opts.requireInitialMode) {
|
|
654
|
+
throw new ACPSessionError(
|
|
655
|
+
"session_create_failed",
|
|
656
|
+
`the agent does not offer the "${want}" session mode this host requires`,
|
|
657
|
+
result
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
if (want && offered && modes?.currentModeId !== want) {
|
|
661
|
+
await executeSetMode(ctx, sessionId, want);
|
|
662
|
+
}
|
|
603
663
|
return sessionId;
|
|
604
664
|
}
|
|
665
|
+
async function executeSteer(ctx, supported, text) {
|
|
666
|
+
if (ctx.closed || !ctx.sessionId || !supported) return "unsupported";
|
|
667
|
+
const result = await ctx.sendRequest(
|
|
668
|
+
ctx.allocId(),
|
|
669
|
+
"_session/steering",
|
|
670
|
+
{
|
|
671
|
+
sessionId: ctx.sessionId,
|
|
672
|
+
prompt: [{ type: "text", text }],
|
|
673
|
+
_meta: { steering: { idleBehavior: "promptRequired" } }
|
|
674
|
+
},
|
|
675
|
+
1e4
|
|
676
|
+
);
|
|
677
|
+
if (isJsonRpcError(result)) {
|
|
678
|
+
throw new ACPSessionError(
|
|
679
|
+
"prompt_failed",
|
|
680
|
+
`_session/steering failed: ${result.message}`,
|
|
681
|
+
result
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const outcome = result?.outcome;
|
|
685
|
+
return outcome === "injected" || outcome === "promptRequired" || outcome === "startedNewTurn" ? outcome : "unsupported";
|
|
686
|
+
}
|
|
605
687
|
|
|
606
688
|
// src/client/acp-session-auth.ts
|
|
607
689
|
function ensureOpen(host) {
|
|
@@ -966,6 +1048,10 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
|
|
|
966
1048
|
return;
|
|
967
1049
|
}
|
|
968
1050
|
if (msg.method === "fs/write_text_file") {
|
|
1051
|
+
if (typeof params.content !== "string") {
|
|
1052
|
+
await sender.sendErrorResponse(id, -32602, "content must be a string");
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
969
1055
|
const authorization = await authorizeAcpCallback(
|
|
970
1056
|
permissionPolicy,
|
|
971
1057
|
{
|
|
@@ -999,7 +1085,7 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
|
|
|
999
1085
|
await fileServer.writeTextFile({
|
|
1000
1086
|
sessionId: params.sessionId ?? "",
|
|
1001
1087
|
path: params.path,
|
|
1002
|
-
content: params.content
|
|
1088
|
+
content: params.content
|
|
1003
1089
|
});
|
|
1004
1090
|
await sender.sendResult(id, {});
|
|
1005
1091
|
}
|
|
@@ -1016,17 +1102,22 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
|
|
|
1016
1102
|
try {
|
|
1017
1103
|
switch (msg.method) {
|
|
1018
1104
|
case "terminal/create": {
|
|
1105
|
+
const env = Array.isArray(params.env) ? params.env.filter(
|
|
1106
|
+
(entry) => !!entry && typeof entry.name === "string" && typeof entry.value === "string"
|
|
1107
|
+
) : [];
|
|
1108
|
+
const envSuffix = env.length > 0 ? ` (env: ${env.map((e) => `${e.name}=${e.value}`).join(" ")})` : "";
|
|
1019
1109
|
const authorization = await authorizeAcpCallback(
|
|
1020
1110
|
permissionPolicy,
|
|
1021
1111
|
{
|
|
1022
1112
|
toolCallId: `acp-terminal-create-${id}`,
|
|
1023
|
-
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
1113
|
+
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim() + envSuffix,
|
|
1024
1114
|
kind: "execute",
|
|
1025
1115
|
rawInput: {
|
|
1026
1116
|
command: params.command,
|
|
1027
1117
|
args: params.args,
|
|
1028
1118
|
cwd: params.cwd,
|
|
1029
|
-
sessionId: params.sessionId
|
|
1119
|
+
sessionId: params.sessionId,
|
|
1120
|
+
...env.length > 0 ? { env } : {}
|
|
1030
1121
|
}
|
|
1031
1122
|
},
|
|
1032
1123
|
callbackOptions2
|
|
@@ -1045,9 +1136,7 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
|
|
|
1045
1136
|
command: String(params.command ?? ""),
|
|
1046
1137
|
args: Array.isArray(params.args) ? params.args : []
|
|
1047
1138
|
};
|
|
1048
|
-
if (
|
|
1049
|
-
createOpts.env = params.env;
|
|
1050
|
-
}
|
|
1139
|
+
if (env.length > 0) createOpts.env = env;
|
|
1051
1140
|
if (typeof params.cwd === "string") {
|
|
1052
1141
|
createOpts.cwd = params.cwd;
|
|
1053
1142
|
}
|
|
@@ -1201,6 +1290,7 @@ function emptyRunResult(stopReason) {
|
|
|
1201
1290
|
function createSessionScratch() {
|
|
1202
1291
|
return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
1203
1292
|
}
|
|
1293
|
+
var MESSAGE_SEPARATOR = "\n\n";
|
|
1204
1294
|
function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
1205
1295
|
const update = msg.params?.update;
|
|
1206
1296
|
if (typeof update !== "object" || update === null) return;
|
|
@@ -1210,6 +1300,10 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
|
1210
1300
|
case "agent_message_chunk": {
|
|
1211
1301
|
const text = extractText(u.content);
|
|
1212
1302
|
if (text) {
|
|
1303
|
+
if (scratch.messageBreak && scratch.text && !/\s$/.test(scratch.text)) {
|
|
1304
|
+
scratch.text += MESSAGE_SEPARATOR;
|
|
1305
|
+
}
|
|
1306
|
+
scratch.messageBreak = false;
|
|
1213
1307
|
scratch.text += text;
|
|
1214
1308
|
emitProgress2({ type: "message", text });
|
|
1215
1309
|
}
|
|
@@ -1234,7 +1328,9 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
|
1234
1328
|
emitProgress2({ type: "plan", entries: u.entries });
|
|
1235
1329
|
}
|
|
1236
1330
|
return;
|
|
1237
|
-
case "usage_update":
|
|
1331
|
+
case "usage_update": {
|
|
1332
|
+
const rateLimit = u._meta?.["_claude/rateLimit"];
|
|
1333
|
+
if (isRecord(rateLimit)) emitProgress2({ type: "rate_limit", info: rateLimit });
|
|
1238
1334
|
if (typeof u.used === "number" && Number.isFinite(u.used) && u.used >= 0 && typeof u.size === "number" && Number.isFinite(u.size) && u.size >= 0) {
|
|
1239
1335
|
const usage = {
|
|
1240
1336
|
used: u.used,
|
|
@@ -1245,6 +1341,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
|
1245
1341
|
emitProgress2({ type: "usage", usage });
|
|
1246
1342
|
}
|
|
1247
1343
|
return;
|
|
1344
|
+
}
|
|
1248
1345
|
case "available_commands_update":
|
|
1249
1346
|
case "current_mode_update":
|
|
1250
1347
|
case "config_option_update":
|
|
@@ -1257,6 +1354,20 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
|
1257
1354
|
return;
|
|
1258
1355
|
}
|
|
1259
1356
|
}
|
|
1357
|
+
function toolOutputText(u) {
|
|
1358
|
+
const joined = (blocks) => blocks.map(extractText).filter((text) => text.length > 0).join("\n");
|
|
1359
|
+
if (Array.isArray(u.content)) {
|
|
1360
|
+
const text = joined(
|
|
1361
|
+
u.content.map(
|
|
1362
|
+
(c) => c && typeof c === "object" && c.type === "content" ? c.content : void 0
|
|
1363
|
+
)
|
|
1364
|
+
);
|
|
1365
|
+
if (text) return text;
|
|
1366
|
+
}
|
|
1367
|
+
if (typeof u.rawOutput === "string") return u.rawOutput || void 0;
|
|
1368
|
+
if (Array.isArray(u.rawOutput)) return joined(u.rawOutput) || void 0;
|
|
1369
|
+
return void 0;
|
|
1370
|
+
}
|
|
1260
1371
|
function captureToolCall(u, isNew, scratch, emitProgress2) {
|
|
1261
1372
|
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
|
|
1262
1373
|
if (!toolCallId) return;
|
|
@@ -1267,12 +1378,18 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
|
|
|
1267
1378
|
kind: typeof u.kind === "string" ? u.kind : prev?.kind,
|
|
1268
1379
|
status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
|
|
1269
1380
|
rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
|
|
1270
|
-
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
|
|
1381
|
+
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput,
|
|
1382
|
+
output: toolOutputText(u) ?? prev?.output
|
|
1271
1383
|
};
|
|
1272
1384
|
scratch.toolCalls.set(toolCallId, record);
|
|
1385
|
+
if (scratch.text) scratch.messageBreak = true;
|
|
1273
1386
|
if (Array.isArray(u.content)) {
|
|
1274
1387
|
for (const c of u.content) {
|
|
1275
1388
|
if (c && typeof c === "object" && c.type === "diff") {
|
|
1389
|
+
const key = `${toolCallId}\0${c.path}\0${c.oldText ?? ""}\0${c.newText}`;
|
|
1390
|
+
scratch.seenDiffs ??= /* @__PURE__ */ new Set();
|
|
1391
|
+
if (scratch.seenDiffs.has(key)) continue;
|
|
1392
|
+
scratch.seenDiffs.add(key);
|
|
1276
1393
|
const diff = {
|
|
1277
1394
|
path: c.path,
|
|
1278
1395
|
oldText: c.oldText,
|
|
@@ -1283,10 +1400,15 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
|
|
|
1283
1400
|
}
|
|
1284
1401
|
}
|
|
1285
1402
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1403
|
+
scratch.announced ??= /* @__PURE__ */ new Set();
|
|
1404
|
+
if (!scratch.announced.has(toolCallId)) {
|
|
1405
|
+
const hasInput = record.rawInput !== void 0 && Object.keys(record.rawInput).length > 0;
|
|
1406
|
+
if (isNew && !hasInput) return;
|
|
1407
|
+
scratch.announced.add(toolCallId);
|
|
1408
|
+
emitProgress2({ type: "tool_call", toolCall: record });
|
|
1409
|
+
if (isNew || record.status !== "completed" && record.status !== "failed") return;
|
|
1410
|
+
}
|
|
1411
|
+
emitProgress2({ type: "tool_call_update", toolCall: record });
|
|
1290
1412
|
}
|
|
1291
1413
|
|
|
1292
1414
|
// src/client/acp-session-messages.ts
|
|
@@ -1318,6 +1440,7 @@ function handleMessage(msg) {
|
|
|
1318
1440
|
}
|
|
1319
1441
|
return;
|
|
1320
1442
|
}
|
|
1443
|
+
touchIdleRequests(this.pending);
|
|
1321
1444
|
if (msg.method === "session/update") {
|
|
1322
1445
|
const updateSessionId = msg.params?.sessionId;
|
|
1323
1446
|
if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
|
|
@@ -1374,6 +1497,7 @@ function handleMessage(msg) {
|
|
|
1374
1497
|
);
|
|
1375
1498
|
return;
|
|
1376
1499
|
}
|
|
1500
|
+
if (msg.method.startsWith("_")) return;
|
|
1377
1501
|
console.warn(
|
|
1378
1502
|
JSON.stringify({
|
|
1379
1503
|
level: "warn",
|
|
@@ -1483,7 +1607,9 @@ async function prompt(host, blocks, signal, onProgress) {
|
|
|
1483
1607
|
sessionId: host.sessionId,
|
|
1484
1608
|
prompt: blocks
|
|
1485
1609
|
},
|
|
1486
|
-
|
|
1610
|
+
// Timed by silence, not length: a working agent keeps it alive.
|
|
1611
|
+
host.promptIdleTimeoutMs,
|
|
1612
|
+
true
|
|
1487
1613
|
),
|
|
1488
1614
|
new Promise((_, reject) => {
|
|
1489
1615
|
rejectTurn = reject;
|
|
@@ -1499,6 +1625,17 @@ async function prompt(host, blocks, signal, onProgress) {
|
|
|
1499
1625
|
if (cancelled || abortedKind) {
|
|
1500
1626
|
return emptyRunResult("cancelled");
|
|
1501
1627
|
}
|
|
1628
|
+
if (err instanceof ACPSessionError && err.kind === "stalled") {
|
|
1629
|
+
if (host.sessionId) {
|
|
1630
|
+
host.transport.send({
|
|
1631
|
+
jsonrpc: "2.0",
|
|
1632
|
+
method: "session/cancel",
|
|
1633
|
+
params: { sessionId: host.sessionId }
|
|
1634
|
+
}).catch(() => {
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
throw err;
|
|
1638
|
+
}
|
|
1502
1639
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1503
1640
|
if (signal.aborted) {
|
|
1504
1641
|
throw new ACPSessionError("aborted", "prompt was aborted by the parent", err);
|
|
@@ -1520,17 +1657,31 @@ async function prompt(host, blocks, signal, onProgress) {
|
|
|
1520
1657
|
}
|
|
1521
1658
|
const stopReason = response.stopReason ?? "end_turn";
|
|
1522
1659
|
const finalText = host.scratch.text;
|
|
1660
|
+
const tokens = readTokenUsage(response.usage);
|
|
1523
1661
|
return {
|
|
1524
1662
|
text: finalText,
|
|
1525
1663
|
stopReason,
|
|
1526
1664
|
hasText: finalText.length > 0,
|
|
1527
1665
|
usage: host.scratch.usage,
|
|
1666
|
+
...tokens ? { tokens } : {},
|
|
1528
1667
|
plan: host.scratch.plan,
|
|
1529
1668
|
toolCalls: [...host.scratch.toolCalls.values()],
|
|
1530
1669
|
diffs: host.scratch.diffs,
|
|
1531
1670
|
thoughts: host.scratch.thoughts
|
|
1532
1671
|
};
|
|
1533
1672
|
}
|
|
1673
|
+
function readTokenUsage(raw) {
|
|
1674
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1675
|
+
const r = raw;
|
|
1676
|
+
const n = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
1677
|
+
if (typeof r.inputTokens !== "number" && typeof r.outputTokens !== "number") return void 0;
|
|
1678
|
+
return {
|
|
1679
|
+
input: n(r.inputTokens),
|
|
1680
|
+
output: n(r.outputTokens),
|
|
1681
|
+
cacheRead: n(r.cachedReadTokens),
|
|
1682
|
+
cacheWrite: n(r.cachedWriteTokens)
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1534
1685
|
|
|
1535
1686
|
// src/client/permission.ts
|
|
1536
1687
|
function pickAllow(options) {
|
|
@@ -2180,6 +2331,7 @@ var ACPSession = class _ACPSession {
|
|
|
2180
2331
|
terminalServer;
|
|
2181
2332
|
permissionPolicy;
|
|
2182
2333
|
timeoutMs;
|
|
2334
|
+
promptIdleTimeoutMs;
|
|
2183
2335
|
opts;
|
|
2184
2336
|
transportOff = null;
|
|
2185
2337
|
callbackAbort = new AbortController();
|
|
@@ -2195,12 +2347,15 @@ var ACPSession = class _ACPSession {
|
|
|
2195
2347
|
agentCapabilities = {};
|
|
2196
2348
|
agentInfo = null;
|
|
2197
2349
|
authMethods = [];
|
|
2350
|
+
/** The agent accepts `_session/steering` (see executeSteer). */
|
|
2351
|
+
steeringSupported = false;
|
|
2198
2352
|
/** Protocol version negotiated with the agent during initialize. */
|
|
2199
2353
|
negotiatedVersion = ACP_PROTOCOL_VERSION;
|
|
2200
2354
|
constructor(opts, transport) {
|
|
2201
2355
|
this.opts = opts;
|
|
2202
2356
|
this.transport = transport;
|
|
2203
2357
|
this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
2358
|
+
this.promptIdleTimeoutMs = opts.promptIdleTimeoutMs ?? 10 * 6e4;
|
|
2204
2359
|
const fsOpts = {
|
|
2205
2360
|
projectRoot: opts.projectRoot
|
|
2206
2361
|
};
|
|
@@ -2355,6 +2510,8 @@ var ACPSession = class _ACPSession {
|
|
|
2355
2510
|
}
|
|
2356
2511
|
this.negotiatedVersion = r.protocolVersion;
|
|
2357
2512
|
this.agentCapabilities = r.agentCapabilities ?? {};
|
|
2513
|
+
const meta = result._meta;
|
|
2514
|
+
this.steeringSupported = meta?.steering?.supported === true;
|
|
2358
2515
|
this.agentInfo = r.agentInfo ?? null;
|
|
2359
2516
|
this.authMethods = r.authMethods ?? [];
|
|
2360
2517
|
this.state = "ready";
|
|
@@ -2399,6 +2556,10 @@ var ACPSession = class _ACPSession {
|
|
|
2399
2556
|
closeSession: () => this.closeSession()
|
|
2400
2557
|
};
|
|
2401
2558
|
}
|
|
2559
|
+
/** Start (or, with `resumeSessionId`, resume) the agent session now, not at the first prompt. */
|
|
2560
|
+
async openSession() {
|
|
2561
|
+
return executeOpenSession(this.opContext(), () => this.createSessionWithAuth());
|
|
2562
|
+
}
|
|
2402
2563
|
async loadSession(sessionId, mcpServers, cwd) {
|
|
2403
2564
|
return executeLoadSession(this.opContext(), sessionId, mcpServers, cwd);
|
|
2404
2565
|
}
|
|
@@ -2432,6 +2593,14 @@ var ACPSession = class _ACPSession {
|
|
|
2432
2593
|
async disableProvider() {
|
|
2433
2594
|
return executeDisableProvider(this.opContext());
|
|
2434
2595
|
}
|
|
2596
|
+
/** Whether {@link steer} can reach the agent. */
|
|
2597
|
+
canSteer() {
|
|
2598
|
+
return this.steeringSupported;
|
|
2599
|
+
}
|
|
2600
|
+
/** Inject a message into the agent's running turn. */
|
|
2601
|
+
async steer(text) {
|
|
2602
|
+
return executeSteer(this.opContext(), this.steeringSupported, text);
|
|
2603
|
+
}
|
|
2435
2604
|
// ──────────────────────────────────────────────────────────────────────
|
|
2436
2605
|
// Prompt
|
|
2437
2606
|
// ──────────────────────────────────────────────────────────────────────
|
|
@@ -2450,14 +2619,14 @@ var ACPSession = class _ACPSession {
|
|
|
2450
2619
|
cancelLateSession(createPromise) {
|
|
2451
2620
|
cancelLateAcpSession(this.transport, createPromise);
|
|
2452
2621
|
}
|
|
2453
|
-
async closeSession() {
|
|
2622
|
+
async closeSession(graceMs = 1e4) {
|
|
2454
2623
|
if (!this.sessionId) return;
|
|
2455
2624
|
const sid = this.sessionId;
|
|
2456
2625
|
this.sessionId = null;
|
|
2457
2626
|
if (this.agentCapabilities.sessionCapabilities?.close) {
|
|
2458
2627
|
const id = this.allocId();
|
|
2459
2628
|
try {
|
|
2460
|
-
await this.sendRequest(id, "session/close", { sessionId: sid },
|
|
2629
|
+
await this.sendRequest(id, "session/close", { sessionId: sid }, graceMs);
|
|
2461
2630
|
} catch {
|
|
2462
2631
|
}
|
|
2463
2632
|
}
|
|
@@ -2465,7 +2634,12 @@ var ACPSession = class _ACPSession {
|
|
|
2465
2634
|
// ──────────────────────────────────────────────────────────────────────
|
|
2466
2635
|
// Lifecycle — close
|
|
2467
2636
|
// ──────────────────────────────────────────────────────────────────────
|
|
2468
|
-
|
|
2637
|
+
/**
|
|
2638
|
+
* Close the session and stop the agent. `graceMs` bounds how long the
|
|
2639
|
+
* agent may take to answer `session/close` before it is stopped anyway —
|
|
2640
|
+
* shutdown passes a short one so a slow answer cannot outlive the process.
|
|
2641
|
+
*/
|
|
2642
|
+
async close(opts = {}) {
|
|
2469
2643
|
if (this.closed) return;
|
|
2470
2644
|
this.closed = true;
|
|
2471
2645
|
this.state = "closed";
|
|
@@ -2475,7 +2649,7 @@ var ACPSession = class _ACPSession {
|
|
|
2475
2649
|
this.terminalServer.dispose();
|
|
2476
2650
|
if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
|
|
2477
2651
|
try {
|
|
2478
|
-
await this.closeSession();
|
|
2652
|
+
await this.closeSession(opts.graceMs);
|
|
2479
2653
|
} catch {
|
|
2480
2654
|
}
|
|
2481
2655
|
}
|
|
@@ -2499,24 +2673,37 @@ var ACPSession = class _ACPSession {
|
|
|
2499
2673
|
}
|
|
2500
2674
|
throw new ACPSessionError("protocol_error", "no JSON-RPC request id is available");
|
|
2501
2675
|
}
|
|
2502
|
-
async sendRequest(id, method, params, timeoutMs) {
|
|
2676
|
+
async sendRequest(id, method, params, timeoutMs, idle = false) {
|
|
2503
2677
|
return new Promise((resolve3, reject) => {
|
|
2504
2678
|
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
2505
|
-
const
|
|
2679
|
+
const expire = () => {
|
|
2506
2680
|
this.pending.delete(id);
|
|
2507
2681
|
reject(
|
|
2508
|
-
new ACPSessionError(
|
|
2682
|
+
idle ? new ACPSessionError(
|
|
2683
|
+
"stalled",
|
|
2684
|
+
`${method}: no activity from the agent for ${Math.round(effectiveTimeout / 1e3)}s`
|
|
2685
|
+
) : new ACPSessionError(
|
|
2686
|
+
"protocol_error",
|
|
2687
|
+
`${method} timed out after ${effectiveTimeout}ms`
|
|
2688
|
+
)
|
|
2509
2689
|
);
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2690
|
+
};
|
|
2691
|
+
const entry = {
|
|
2512
2692
|
method,
|
|
2513
2693
|
resolve: resolve3,
|
|
2514
2694
|
reject,
|
|
2515
2695
|
timeoutMs: effectiveTimeout,
|
|
2516
|
-
timeoutHandle:
|
|
2517
|
-
}
|
|
2696
|
+
timeoutHandle: setTimeout(expire, effectiveTimeout)
|
|
2697
|
+
};
|
|
2698
|
+
if (idle) {
|
|
2699
|
+
entry.rearm = () => {
|
|
2700
|
+
clearTimeout(entry.timeoutHandle);
|
|
2701
|
+
entry.timeoutHandle = setTimeout(expire, effectiveTimeout);
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
this.pending.set(id, entry);
|
|
2518
2705
|
this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
|
2519
|
-
clearTimeout(
|
|
2706
|
+
clearTimeout(entry.timeoutHandle);
|
|
2520
2707
|
this.pending.delete(id);
|
|
2521
2708
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2522
2709
|
reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
|
|
@@ -2620,13 +2807,74 @@ var ACPSession = class _ACPSession {
|
|
|
2620
2807
|
progressHandler: this.progressHandler,
|
|
2621
2808
|
allocId: this.allocId,
|
|
2622
2809
|
sendRequest: this.sendRequest,
|
|
2623
|
-
|
|
2810
|
+
promptIdleTimeoutMs: this.promptIdleTimeoutMs,
|
|
2624
2811
|
scratch: this.scratch
|
|
2625
2812
|
};
|
|
2626
2813
|
return this;
|
|
2627
2814
|
}
|
|
2628
2815
|
};
|
|
2629
2816
|
|
|
2817
|
+
// src/integration/acp-agent-failure.ts
|
|
2818
|
+
var USAGE_LIMIT_PREFIXES = [
|
|
2819
|
+
"You've hit your",
|
|
2820
|
+
"You've reached your",
|
|
2821
|
+
"You're out of usage credits",
|
|
2822
|
+
"Your org is out of usage",
|
|
2823
|
+
"Your seat type doesn't include usage",
|
|
2824
|
+
"Your seat type doesn't include extra usage",
|
|
2825
|
+
"Your usage allocation has been disabled",
|
|
2826
|
+
"Your group's usage limit is set to $0",
|
|
2827
|
+
"requires usage credits",
|
|
2828
|
+
"You're out of extra usage",
|
|
2829
|
+
"Claude AI usage limit reached"
|
|
2830
|
+
];
|
|
2831
|
+
var DEFAULT_QUOTA_HOLD_MS = 15 * 6e4;
|
|
2832
|
+
function parseQuotaResetAt(text, now = Date.now()) {
|
|
2833
|
+
const epoch = /usage limit reached\|(\d{9,11})/i.exec(text);
|
|
2834
|
+
if (epoch?.[1]) return Number(epoch[1]) * 1e3;
|
|
2835
|
+
const m = /\bresets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i.exec(text);
|
|
2836
|
+
if (!m?.[1]) return void 0;
|
|
2837
|
+
let hour = Number(m[1]);
|
|
2838
|
+
const minute = m[2] ? Number(m[2]) : 0;
|
|
2839
|
+
const meridiem = m[3]?.toLowerCase();
|
|
2840
|
+
if (meridiem === "pm" && hour < 12) hour += 12;
|
|
2841
|
+
if (meridiem === "am" && hour === 12) hour = 0;
|
|
2842
|
+
if (hour > 23 || minute > 59 || !meridiem && !m[2]) return void 0;
|
|
2843
|
+
const at = new Date(now);
|
|
2844
|
+
at.setHours(hour, minute, 0, 0);
|
|
2845
|
+
if (at.getTime() <= now) at.setDate(at.getDate() + 1);
|
|
2846
|
+
return at.getTime();
|
|
2847
|
+
}
|
|
2848
|
+
function classifyAgentFailure(rpc, agent, now = Date.now()) {
|
|
2849
|
+
const text = rpc.message.replace(/^Internal error:\s*/i, "").trim();
|
|
2850
|
+
const errorKind = rpc.data?.errorKind;
|
|
2851
|
+
const isQuota = USAGE_LIMIT_PREFIXES.some((p) => text.startsWith(p) || text.includes(p)) || errorKind === "billing_error" || errorKind === "account_on_hold";
|
|
2852
|
+
if (isQuota) {
|
|
2853
|
+
const resetAt = parseQuotaResetAt(text, now);
|
|
2854
|
+
return {
|
|
2855
|
+
kind: "provider_quota",
|
|
2856
|
+
message: `${agent}: usage quota spent \u2014 ${text || "the account has no usage left"}`,
|
|
2857
|
+
retryable: false,
|
|
2858
|
+
backoffMs: resetAt !== void 0 ? Math.max(0, resetAt - now) : DEFAULT_QUOTA_HOLD_MS
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
const message = `${agent}: ${text}`;
|
|
2862
|
+
switch (errorKind) {
|
|
2863
|
+
case "rate_limit":
|
|
2864
|
+
return { kind: "provider_rate_limit", message, retryable: true, backoffMs: 3e4 };
|
|
2865
|
+
case "overloaded":
|
|
2866
|
+
case "server_error":
|
|
2867
|
+
return { kind: "provider_5xx", message, retryable: true, backoffMs: 1e4 };
|
|
2868
|
+
case "authentication_failed":
|
|
2869
|
+
case "oauth_org_not_allowed":
|
|
2870
|
+
case "verification_required":
|
|
2871
|
+
case "cloud_credential_error":
|
|
2872
|
+
return { kind: "provider_auth", message, retryable: false };
|
|
2873
|
+
default:
|
|
2874
|
+
return rpc.code === -32e3 ? { kind: "provider_auth", message, retryable: false } : void 0;
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2630
2878
|
// src/integration/acp-subagent-runner.ts
|
|
2631
2879
|
async function makeACPSubagentRunner(options) {
|
|
2632
2880
|
const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
|
|
@@ -2639,13 +2887,37 @@ async function makeACPSubagentRunner(options) {
|
|
|
2639
2887
|
};
|
|
2640
2888
|
return wrappedRunner;
|
|
2641
2889
|
}
|
|
2890
|
+
var sessionCostSeen = /* @__PURE__ */ new WeakMap();
|
|
2891
|
+
function recordTurnUsage(ctx, session, result) {
|
|
2892
|
+
const cost = result.usage?.cost;
|
|
2893
|
+
const total = cost?.currency === "USD" && Number.isFinite(cost.amount) && cost.amount >= 0 ? cost.amount : void 0;
|
|
2894
|
+
const turnCost = total === void 0 ? 0 : Math.max(0, total - (sessionCostSeen.get(session) ?? 0));
|
|
2895
|
+
if (total !== void 0) sessionCostSeen.set(session, total);
|
|
2896
|
+
const t = result.tokens;
|
|
2897
|
+
if (!t && turnCost === 0) return void 0;
|
|
2898
|
+
try {
|
|
2899
|
+
ctx.budget.recordUsage(
|
|
2900
|
+
{
|
|
2901
|
+
input: t?.input ?? 0,
|
|
2902
|
+
output: t?.output ?? 0,
|
|
2903
|
+
cacheRead: t?.cacheRead ?? 0,
|
|
2904
|
+
cacheWrite: t?.cacheWrite ?? 0
|
|
2905
|
+
},
|
|
2906
|
+
turnCost
|
|
2907
|
+
);
|
|
2908
|
+
} catch {
|
|
2909
|
+
}
|
|
2910
|
+
return { type: "turn_usage", ...t ? { tokens: t } : {}, costUsd: turnCost };
|
|
2911
|
+
}
|
|
2642
2912
|
async function makeACPSubagentRunnerWithStop(options) {
|
|
2643
2913
|
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
2644
2914
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
2645
2915
|
const persistent = options.persistent === true;
|
|
2646
2916
|
let shared = null;
|
|
2647
2917
|
let turns = Promise.resolve();
|
|
2648
|
-
const startSession = async () => {
|
|
2918
|
+
const startSession = async (setup) => {
|
|
2919
|
+
const mcpServers = setup?.mcpServers && setup.mcpServers.length > 0 ? [...options.mcpServers ?? [], ...setup.mcpServers] : options.mcpServers;
|
|
2920
|
+
const permissionPolicy = setup?.permissionPolicy ?? options.permissionPolicy;
|
|
2649
2921
|
return ACPSession.start({
|
|
2650
2922
|
command: options.command,
|
|
2651
2923
|
...options.args !== void 0 ? { args: options.args } : {},
|
|
@@ -2653,23 +2925,29 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2653
2925
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
2654
2926
|
projectRoot,
|
|
2655
2927
|
timeoutMs,
|
|
2928
|
+
...options.promptIdleTimeoutMs !== void 0 ? { promptIdleTimeoutMs: options.promptIdleTimeoutMs } : {},
|
|
2656
2929
|
role: options.role,
|
|
2657
|
-
...
|
|
2658
|
-
...
|
|
2930
|
+
...permissionPolicy !== void 0 ? { permissionPolicy } : {},
|
|
2931
|
+
...mcpServers !== void 0 ? { mcpServers } : {},
|
|
2932
|
+
...setup?.meta ? { sessionMeta: setup.meta } : {},
|
|
2933
|
+
...options.initialModeId ? { initialModeId: options.initialModeId } : {},
|
|
2934
|
+
...options.requireInitialMode ? { requireInitialMode: true } : {}
|
|
2659
2935
|
});
|
|
2660
2936
|
};
|
|
2661
2937
|
const runner = async (task, ctx) => {
|
|
2662
2938
|
let session;
|
|
2939
|
+
let setup;
|
|
2663
2940
|
try {
|
|
2941
|
+
setup = options.sessionSetup?.(task, ctx);
|
|
2664
2942
|
if (persistent) {
|
|
2665
|
-
shared ??= startSession();
|
|
2943
|
+
shared ??= startSession(setup);
|
|
2666
2944
|
const starting = shared;
|
|
2667
2945
|
session = await starting.catch((err) => {
|
|
2668
2946
|
if (shared === starting) shared = null;
|
|
2669
2947
|
throw err;
|
|
2670
2948
|
});
|
|
2671
2949
|
} else {
|
|
2672
|
-
session = await startSession();
|
|
2950
|
+
session = await startSession(setup);
|
|
2673
2951
|
}
|
|
2674
2952
|
} catch (err) {
|
|
2675
2953
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
@@ -2686,14 +2964,20 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2686
2964
|
}
|
|
2687
2965
|
};
|
|
2688
2966
|
let result;
|
|
2967
|
+
let endLive;
|
|
2689
2968
|
try {
|
|
2690
|
-
|
|
2969
|
+
if (session.canSteer()) endLive = options.onTurnLive?.(ctx, task, (t) => session.steer(t));
|
|
2970
|
+
const prompt2 = setup?.promptPrefix ? `${setup.promptPrefix}
|
|
2971
|
+
|
|
2972
|
+
${task.description}` : task.description;
|
|
2973
|
+
const turn = () => session.prompt([textContent(prompt2)], ctx.signal, onProgress);
|
|
2691
2974
|
const run = persistent ? turns.then(turn, turn) : turn();
|
|
2692
2975
|
if (persistent) turns = run;
|
|
2693
2976
|
result = await run;
|
|
2694
2977
|
} catch (err) {
|
|
2695
2978
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
2696
2979
|
} finally {
|
|
2980
|
+
endLive?.();
|
|
2697
2981
|
if (!persistent) {
|
|
2698
2982
|
try {
|
|
2699
2983
|
await session.close();
|
|
@@ -2701,6 +2985,8 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2701
2985
|
}
|
|
2702
2986
|
}
|
|
2703
2987
|
}
|
|
2988
|
+
const turnUsage = recordTurnUsage(ctx, session, result);
|
|
2989
|
+
if (turnUsage) onProgress(turnUsage);
|
|
2704
2990
|
const failure = acpTurnFailure(result, options.role ?? "acp-subagent");
|
|
2705
2991
|
if (failure) throw failure;
|
|
2706
2992
|
return {
|
|
@@ -2721,7 +3007,20 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2721
3007
|
};
|
|
2722
3008
|
return { runner, stop };
|
|
2723
3009
|
}
|
|
3010
|
+
function agentRpcError(err) {
|
|
3011
|
+
let cur = err;
|
|
3012
|
+
for (let depth = 0; depth < 4 && cur; depth++) {
|
|
3013
|
+
if (isJsonRpcError(cur)) return cur;
|
|
3014
|
+
cur = cur.cause;
|
|
3015
|
+
}
|
|
3016
|
+
return void 0;
|
|
3017
|
+
}
|
|
2724
3018
|
function acpErrorToSubagentError(err, subagentId) {
|
|
3019
|
+
const rpc = agentRpcError(err);
|
|
3020
|
+
if (rpc) {
|
|
3021
|
+
const classified = classifyAgentFailure(rpc, subagentId);
|
|
3022
|
+
if (classified) return classified;
|
|
3023
|
+
}
|
|
2725
3024
|
if (err instanceof ACPSessionError) {
|
|
2726
3025
|
const kind = mapACPKind(err.kind);
|
|
2727
3026
|
return {
|
|
@@ -2789,6 +3088,9 @@ function mapACPKind(acpKind) {
|
|
|
2789
3088
|
return "bridge_failed";
|
|
2790
3089
|
case "aborted":
|
|
2791
3090
|
return "aborted_by_parent";
|
|
3091
|
+
// The agent went silent mid-turn and the turn was cancelled.
|
|
3092
|
+
case "stalled":
|
|
3093
|
+
return "budget_timeout";
|
|
2792
3094
|
case "closed":
|
|
2793
3095
|
case "unsupported_capability":
|
|
2794
3096
|
return "unknown";
|