@wrongstack/acp 1.1.2 → 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 +330 -35
- package/dist/index.d.ts +3 -1
- package/dist/index.js +334 -36
- 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) {
|
|
@@ -1208,6 +1290,7 @@ function emptyRunResult(stopReason) {
|
|
|
1208
1290
|
function createSessionScratch() {
|
|
1209
1291
|
return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
1210
1292
|
}
|
|
1293
|
+
var MESSAGE_SEPARATOR = "\n\n";
|
|
1211
1294
|
function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
1212
1295
|
const update = msg.params?.update;
|
|
1213
1296
|
if (typeof update !== "object" || update === null) return;
|
|
@@ -1217,6 +1300,10 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
|
1217
1300
|
case "agent_message_chunk": {
|
|
1218
1301
|
const text = extractText(u.content);
|
|
1219
1302
|
if (text) {
|
|
1303
|
+
if (scratch.messageBreak && scratch.text && !/\s$/.test(scratch.text)) {
|
|
1304
|
+
scratch.text += MESSAGE_SEPARATOR;
|
|
1305
|
+
}
|
|
1306
|
+
scratch.messageBreak = false;
|
|
1220
1307
|
scratch.text += text;
|
|
1221
1308
|
emitProgress2({ type: "message", text });
|
|
1222
1309
|
}
|
|
@@ -1241,7 +1328,9 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
|
1241
1328
|
emitProgress2({ type: "plan", entries: u.entries });
|
|
1242
1329
|
}
|
|
1243
1330
|
return;
|
|
1244
|
-
case "usage_update":
|
|
1331
|
+
case "usage_update": {
|
|
1332
|
+
const rateLimit = u._meta?.["_claude/rateLimit"];
|
|
1333
|
+
if (isRecord(rateLimit)) emitProgress2({ type: "rate_limit", info: rateLimit });
|
|
1245
1334
|
if (typeof u.used === "number" && Number.isFinite(u.used) && u.used >= 0 && typeof u.size === "number" && Number.isFinite(u.size) && u.size >= 0) {
|
|
1246
1335
|
const usage = {
|
|
1247
1336
|
used: u.used,
|
|
@@ -1252,6 +1341,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
|
1252
1341
|
emitProgress2({ type: "usage", usage });
|
|
1253
1342
|
}
|
|
1254
1343
|
return;
|
|
1344
|
+
}
|
|
1255
1345
|
case "available_commands_update":
|
|
1256
1346
|
case "current_mode_update":
|
|
1257
1347
|
case "config_option_update":
|
|
@@ -1264,6 +1354,20 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
|
|
|
1264
1354
|
return;
|
|
1265
1355
|
}
|
|
1266
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
|
+
}
|
|
1267
1371
|
function captureToolCall(u, isNew, scratch, emitProgress2) {
|
|
1268
1372
|
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
|
|
1269
1373
|
if (!toolCallId) return;
|
|
@@ -1274,12 +1378,18 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
|
|
|
1274
1378
|
kind: typeof u.kind === "string" ? u.kind : prev?.kind,
|
|
1275
1379
|
status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
|
|
1276
1380
|
rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
|
|
1277
|
-
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
|
|
1381
|
+
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput,
|
|
1382
|
+
output: toolOutputText(u) ?? prev?.output
|
|
1278
1383
|
};
|
|
1279
1384
|
scratch.toolCalls.set(toolCallId, record);
|
|
1385
|
+
if (scratch.text) scratch.messageBreak = true;
|
|
1280
1386
|
if (Array.isArray(u.content)) {
|
|
1281
1387
|
for (const c of u.content) {
|
|
1282
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);
|
|
1283
1393
|
const diff = {
|
|
1284
1394
|
path: c.path,
|
|
1285
1395
|
oldText: c.oldText,
|
|
@@ -1290,10 +1400,15 @@ function captureToolCall(u, isNew, scratch, emitProgress2) {
|
|
|
1290
1400
|
}
|
|
1291
1401
|
}
|
|
1292
1402
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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 });
|
|
1297
1412
|
}
|
|
1298
1413
|
|
|
1299
1414
|
// src/client/acp-session-messages.ts
|
|
@@ -1325,6 +1440,7 @@ function handleMessage(msg) {
|
|
|
1325
1440
|
}
|
|
1326
1441
|
return;
|
|
1327
1442
|
}
|
|
1443
|
+
touchIdleRequests(this.pending);
|
|
1328
1444
|
if (msg.method === "session/update") {
|
|
1329
1445
|
const updateSessionId = msg.params?.sessionId;
|
|
1330
1446
|
if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
|
|
@@ -1381,6 +1497,7 @@ function handleMessage(msg) {
|
|
|
1381
1497
|
);
|
|
1382
1498
|
return;
|
|
1383
1499
|
}
|
|
1500
|
+
if (msg.method.startsWith("_")) return;
|
|
1384
1501
|
console.warn(
|
|
1385
1502
|
JSON.stringify({
|
|
1386
1503
|
level: "warn",
|
|
@@ -1490,7 +1607,9 @@ async function prompt(host, blocks, signal, onProgress) {
|
|
|
1490
1607
|
sessionId: host.sessionId,
|
|
1491
1608
|
prompt: blocks
|
|
1492
1609
|
},
|
|
1493
|
-
|
|
1610
|
+
// Timed by silence, not length: a working agent keeps it alive.
|
|
1611
|
+
host.promptIdleTimeoutMs,
|
|
1612
|
+
true
|
|
1494
1613
|
),
|
|
1495
1614
|
new Promise((_, reject) => {
|
|
1496
1615
|
rejectTurn = reject;
|
|
@@ -1506,6 +1625,17 @@ async function prompt(host, blocks, signal, onProgress) {
|
|
|
1506
1625
|
if (cancelled || abortedKind) {
|
|
1507
1626
|
return emptyRunResult("cancelled");
|
|
1508
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
|
+
}
|
|
1509
1639
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1510
1640
|
if (signal.aborted) {
|
|
1511
1641
|
throw new ACPSessionError("aborted", "prompt was aborted by the parent", err);
|
|
@@ -1527,17 +1657,31 @@ async function prompt(host, blocks, signal, onProgress) {
|
|
|
1527
1657
|
}
|
|
1528
1658
|
const stopReason = response.stopReason ?? "end_turn";
|
|
1529
1659
|
const finalText = host.scratch.text;
|
|
1660
|
+
const tokens = readTokenUsage(response.usage);
|
|
1530
1661
|
return {
|
|
1531
1662
|
text: finalText,
|
|
1532
1663
|
stopReason,
|
|
1533
1664
|
hasText: finalText.length > 0,
|
|
1534
1665
|
usage: host.scratch.usage,
|
|
1666
|
+
...tokens ? { tokens } : {},
|
|
1535
1667
|
plan: host.scratch.plan,
|
|
1536
1668
|
toolCalls: [...host.scratch.toolCalls.values()],
|
|
1537
1669
|
diffs: host.scratch.diffs,
|
|
1538
1670
|
thoughts: host.scratch.thoughts
|
|
1539
1671
|
};
|
|
1540
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
|
+
}
|
|
1541
1685
|
|
|
1542
1686
|
// src/client/permission.ts
|
|
1543
1687
|
function pickAllow(options) {
|
|
@@ -2187,6 +2331,7 @@ var ACPSession = class _ACPSession {
|
|
|
2187
2331
|
terminalServer;
|
|
2188
2332
|
permissionPolicy;
|
|
2189
2333
|
timeoutMs;
|
|
2334
|
+
promptIdleTimeoutMs;
|
|
2190
2335
|
opts;
|
|
2191
2336
|
transportOff = null;
|
|
2192
2337
|
callbackAbort = new AbortController();
|
|
@@ -2202,12 +2347,15 @@ var ACPSession = class _ACPSession {
|
|
|
2202
2347
|
agentCapabilities = {};
|
|
2203
2348
|
agentInfo = null;
|
|
2204
2349
|
authMethods = [];
|
|
2350
|
+
/** The agent accepts `_session/steering` (see executeSteer). */
|
|
2351
|
+
steeringSupported = false;
|
|
2205
2352
|
/** Protocol version negotiated with the agent during initialize. */
|
|
2206
2353
|
negotiatedVersion = ACP_PROTOCOL_VERSION;
|
|
2207
2354
|
constructor(opts, transport) {
|
|
2208
2355
|
this.opts = opts;
|
|
2209
2356
|
this.transport = transport;
|
|
2210
2357
|
this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
2358
|
+
this.promptIdleTimeoutMs = opts.promptIdleTimeoutMs ?? 10 * 6e4;
|
|
2211
2359
|
const fsOpts = {
|
|
2212
2360
|
projectRoot: opts.projectRoot
|
|
2213
2361
|
};
|
|
@@ -2362,6 +2510,8 @@ var ACPSession = class _ACPSession {
|
|
|
2362
2510
|
}
|
|
2363
2511
|
this.negotiatedVersion = r.protocolVersion;
|
|
2364
2512
|
this.agentCapabilities = r.agentCapabilities ?? {};
|
|
2513
|
+
const meta = result._meta;
|
|
2514
|
+
this.steeringSupported = meta?.steering?.supported === true;
|
|
2365
2515
|
this.agentInfo = r.agentInfo ?? null;
|
|
2366
2516
|
this.authMethods = r.authMethods ?? [];
|
|
2367
2517
|
this.state = "ready";
|
|
@@ -2406,6 +2556,10 @@ var ACPSession = class _ACPSession {
|
|
|
2406
2556
|
closeSession: () => this.closeSession()
|
|
2407
2557
|
};
|
|
2408
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
|
+
}
|
|
2409
2563
|
async loadSession(sessionId, mcpServers, cwd) {
|
|
2410
2564
|
return executeLoadSession(this.opContext(), sessionId, mcpServers, cwd);
|
|
2411
2565
|
}
|
|
@@ -2439,6 +2593,14 @@ var ACPSession = class _ACPSession {
|
|
|
2439
2593
|
async disableProvider() {
|
|
2440
2594
|
return executeDisableProvider(this.opContext());
|
|
2441
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
|
+
}
|
|
2442
2604
|
// ──────────────────────────────────────────────────────────────────────
|
|
2443
2605
|
// Prompt
|
|
2444
2606
|
// ──────────────────────────────────────────────────────────────────────
|
|
@@ -2457,14 +2619,14 @@ var ACPSession = class _ACPSession {
|
|
|
2457
2619
|
cancelLateSession(createPromise) {
|
|
2458
2620
|
cancelLateAcpSession(this.transport, createPromise);
|
|
2459
2621
|
}
|
|
2460
|
-
async closeSession() {
|
|
2622
|
+
async closeSession(graceMs = 1e4) {
|
|
2461
2623
|
if (!this.sessionId) return;
|
|
2462
2624
|
const sid = this.sessionId;
|
|
2463
2625
|
this.sessionId = null;
|
|
2464
2626
|
if (this.agentCapabilities.sessionCapabilities?.close) {
|
|
2465
2627
|
const id = this.allocId();
|
|
2466
2628
|
try {
|
|
2467
|
-
await this.sendRequest(id, "session/close", { sessionId: sid },
|
|
2629
|
+
await this.sendRequest(id, "session/close", { sessionId: sid }, graceMs);
|
|
2468
2630
|
} catch {
|
|
2469
2631
|
}
|
|
2470
2632
|
}
|
|
@@ -2472,7 +2634,12 @@ var ACPSession = class _ACPSession {
|
|
|
2472
2634
|
// ──────────────────────────────────────────────────────────────────────
|
|
2473
2635
|
// Lifecycle — close
|
|
2474
2636
|
// ──────────────────────────────────────────────────────────────────────
|
|
2475
|
-
|
|
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 = {}) {
|
|
2476
2643
|
if (this.closed) return;
|
|
2477
2644
|
this.closed = true;
|
|
2478
2645
|
this.state = "closed";
|
|
@@ -2482,7 +2649,7 @@ var ACPSession = class _ACPSession {
|
|
|
2482
2649
|
this.terminalServer.dispose();
|
|
2483
2650
|
if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
|
|
2484
2651
|
try {
|
|
2485
|
-
await this.closeSession();
|
|
2652
|
+
await this.closeSession(opts.graceMs);
|
|
2486
2653
|
} catch {
|
|
2487
2654
|
}
|
|
2488
2655
|
}
|
|
@@ -2506,24 +2673,37 @@ var ACPSession = class _ACPSession {
|
|
|
2506
2673
|
}
|
|
2507
2674
|
throw new ACPSessionError("protocol_error", "no JSON-RPC request id is available");
|
|
2508
2675
|
}
|
|
2509
|
-
async sendRequest(id, method, params, timeoutMs) {
|
|
2676
|
+
async sendRequest(id, method, params, timeoutMs, idle = false) {
|
|
2510
2677
|
return new Promise((resolve3, reject) => {
|
|
2511
2678
|
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
2512
|
-
const
|
|
2679
|
+
const expire = () => {
|
|
2513
2680
|
this.pending.delete(id);
|
|
2514
2681
|
reject(
|
|
2515
|
-
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
|
+
)
|
|
2516
2689
|
);
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2690
|
+
};
|
|
2691
|
+
const entry = {
|
|
2519
2692
|
method,
|
|
2520
2693
|
resolve: resolve3,
|
|
2521
2694
|
reject,
|
|
2522
2695
|
timeoutMs: effectiveTimeout,
|
|
2523
|
-
timeoutHandle:
|
|
2524
|
-
}
|
|
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);
|
|
2525
2705
|
this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
|
2526
|
-
clearTimeout(
|
|
2706
|
+
clearTimeout(entry.timeoutHandle);
|
|
2527
2707
|
this.pending.delete(id);
|
|
2528
2708
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2529
2709
|
reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
|
|
@@ -2627,13 +2807,74 @@ var ACPSession = class _ACPSession {
|
|
|
2627
2807
|
progressHandler: this.progressHandler,
|
|
2628
2808
|
allocId: this.allocId,
|
|
2629
2809
|
sendRequest: this.sendRequest,
|
|
2630
|
-
|
|
2810
|
+
promptIdleTimeoutMs: this.promptIdleTimeoutMs,
|
|
2631
2811
|
scratch: this.scratch
|
|
2632
2812
|
};
|
|
2633
2813
|
return this;
|
|
2634
2814
|
}
|
|
2635
2815
|
};
|
|
2636
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
|
+
|
|
2637
2878
|
// src/integration/acp-subagent-runner.ts
|
|
2638
2879
|
async function makeACPSubagentRunner(options) {
|
|
2639
2880
|
const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
|
|
@@ -2646,13 +2887,37 @@ async function makeACPSubagentRunner(options) {
|
|
|
2646
2887
|
};
|
|
2647
2888
|
return wrappedRunner;
|
|
2648
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
|
+
}
|
|
2649
2912
|
async function makeACPSubagentRunnerWithStop(options) {
|
|
2650
2913
|
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
2651
2914
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
2652
2915
|
const persistent = options.persistent === true;
|
|
2653
2916
|
let shared = null;
|
|
2654
2917
|
let turns = Promise.resolve();
|
|
2655
|
-
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;
|
|
2656
2921
|
return ACPSession.start({
|
|
2657
2922
|
command: options.command,
|
|
2658
2923
|
...options.args !== void 0 ? { args: options.args } : {},
|
|
@@ -2660,23 +2925,29 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2660
2925
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
2661
2926
|
projectRoot,
|
|
2662
2927
|
timeoutMs,
|
|
2928
|
+
...options.promptIdleTimeoutMs !== void 0 ? { promptIdleTimeoutMs: options.promptIdleTimeoutMs } : {},
|
|
2663
2929
|
role: options.role,
|
|
2664
|
-
...
|
|
2665
|
-
...
|
|
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 } : {}
|
|
2666
2935
|
});
|
|
2667
2936
|
};
|
|
2668
2937
|
const runner = async (task, ctx) => {
|
|
2669
2938
|
let session;
|
|
2939
|
+
let setup;
|
|
2670
2940
|
try {
|
|
2941
|
+
setup = options.sessionSetup?.(task, ctx);
|
|
2671
2942
|
if (persistent) {
|
|
2672
|
-
shared ??= startSession();
|
|
2943
|
+
shared ??= startSession(setup);
|
|
2673
2944
|
const starting = shared;
|
|
2674
2945
|
session = await starting.catch((err) => {
|
|
2675
2946
|
if (shared === starting) shared = null;
|
|
2676
2947
|
throw err;
|
|
2677
2948
|
});
|
|
2678
2949
|
} else {
|
|
2679
|
-
session = await startSession();
|
|
2950
|
+
session = await startSession(setup);
|
|
2680
2951
|
}
|
|
2681
2952
|
} catch (err) {
|
|
2682
2953
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
@@ -2693,14 +2964,20 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2693
2964
|
}
|
|
2694
2965
|
};
|
|
2695
2966
|
let result;
|
|
2967
|
+
let endLive;
|
|
2696
2968
|
try {
|
|
2697
|
-
|
|
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);
|
|
2698
2974
|
const run = persistent ? turns.then(turn, turn) : turn();
|
|
2699
2975
|
if (persistent) turns = run;
|
|
2700
2976
|
result = await run;
|
|
2701
2977
|
} catch (err) {
|
|
2702
2978
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
2703
2979
|
} finally {
|
|
2980
|
+
endLive?.();
|
|
2704
2981
|
if (!persistent) {
|
|
2705
2982
|
try {
|
|
2706
2983
|
await session.close();
|
|
@@ -2708,6 +2985,8 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2708
2985
|
}
|
|
2709
2986
|
}
|
|
2710
2987
|
}
|
|
2988
|
+
const turnUsage = recordTurnUsage(ctx, session, result);
|
|
2989
|
+
if (turnUsage) onProgress(turnUsage);
|
|
2711
2990
|
const failure = acpTurnFailure(result, options.role ?? "acp-subagent");
|
|
2712
2991
|
if (failure) throw failure;
|
|
2713
2992
|
return {
|
|
@@ -2728,7 +3007,20 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2728
3007
|
};
|
|
2729
3008
|
return { runner, stop };
|
|
2730
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
|
+
}
|
|
2731
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
|
+
}
|
|
2732
3024
|
if (err instanceof ACPSessionError) {
|
|
2733
3025
|
const kind = mapACPKind(err.kind);
|
|
2734
3026
|
return {
|
|
@@ -2796,6 +3088,9 @@ function mapACPKind(acpKind) {
|
|
|
2796
3088
|
return "bridge_failed";
|
|
2797
3089
|
case "aborted":
|
|
2798
3090
|
return "aborted_by_parent";
|
|
3091
|
+
// The agent went silent mid-turn and the turn was cancelled.
|
|
3092
|
+
case "stalled":
|
|
3093
|
+
return "budget_timeout";
|
|
2799
3094
|
case "closed":
|
|
2800
3095
|
case "unsupported_capability":
|
|
2801
3096
|
return "unknown";
|