@agentclientprotocol/claude-agent-acp 0.89.2-preview.2 → 0.89.2-preview.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -8
- package/dist/acp-agent.d.ts +131 -23
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +879 -239
- package/dist/acp-subagents.d.ts +16 -6
- package/dist/acp-subagents.d.ts.map +1 -1
- package/dist/acp-subagents.js +8 -2
- package/dist/async-tasks.d.ts +4 -1
- package/dist/async-tasks.d.ts.map +1 -1
- package/dist/auth-status.d.ts +2 -2
- package/dist/auth-status.d.ts.map +1 -1
- package/dist/auth-status.js +18 -4
- package/dist/exit-plan.d.ts +17 -0
- package/dist/exit-plan.d.ts.map +1 -1
- package/dist/exit-plan.js +34 -0
- package/dist/native-subagents.d.ts +190 -11
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +419 -40
- package/dist/settings.d.ts +6 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +13 -0
- package/dist/subagent-history.d.ts +27 -2
- package/dist/subagent-history.d.ts.map +1 -1
- package/dist/subagent-history.js +38 -10
- package/dist/subagent-replay.d.ts +108 -0
- package/dist/subagent-replay.d.ts.map +1 -0
- package/dist/subagent-replay.js +261 -0
- package/dist/tool-calls/background.d.ts +9 -0
- package/dist/tool-calls/background.d.ts.map +1 -1
- package/dist/tool-calls/background.js +25 -0
- package/dist/tool-calls/renderer.d.ts +32 -7
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +54 -21
- package/dist/turn-events.d.ts +39 -17
- package/dist/turn-events.d.ts.map +1 -1
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +52 -14
- package/dist/v2/initialize.d.ts.map +1 -1
- package/dist/v2/initialize.js +10 -1
- package/dist/v2/prompt.d.ts +29 -18
- package/dist/v2/prompt.d.ts.map +1 -1
- package/dist/v2/prompt.js +54 -43
- package/dist/v2/session-update.d.ts +11 -0
- package/dist/v2/session-update.d.ts.map +1 -1
- package/dist/v2/session-update.js +67 -0
- package/dist/v2/session.d.ts +3 -12
- package/dist/v2/session.d.ts.map +1 -1
- package/dist/v2/session.js +5 -18
- package/dist/v2/tool-call-meta.d.ts +44 -0
- package/dist/v2/tool-call-meta.d.ts.map +1 -0
- package/dist/v2/tool-call-meta.js +72 -0
- package/package.json +1 -1
package/dist/acp-agent.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { agent as acpAgent, CreateElicitationResponse, methods, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk";
|
|
2
|
-
import {
|
|
2
|
+
import { filterEscalatingDefaultMode, getSessionMessages, listSessions, query, resolveSettings, } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { GOAL_ACTIONS, GOAL_CONTROL_METHOD, GOAL_EXTENSION_VERSION, goalUpdateFromPrompt, parseGoalRequest, toGoalSnapshot, } from "./goal-extension.js";
|
|
4
4
|
import { sanitizeTitle, SessionTitles } from "./session-titles.js";
|
|
5
|
-
import { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, asSdkSessionNotification, clientSupportsSubagents, } from "./acp-subagents.js";
|
|
6
|
-
import { isNativeSubagentControlTool, isNativeSubagentControlUpdate, NativeSubagentRuntime, nativeSubagentState, resumedNativeSubagentId, sendMessageResumePrompt, } from "./native-subagents.js";
|
|
5
|
+
import { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, asSdkSessionNotification, clientSupportsSubagents, subagentForm, } from "./acp-subagents.js";
|
|
6
|
+
import { isNativeSubagentControlTool, isNativeSubagentControlUpdate, NativeSubagentRuntime, nativeSubagentState, resumedNativeSubagentId, sendMessageResume, sendMessageResumePrompt, } from "./native-subagents.js";
|
|
7
7
|
import { AIR_ASYNC_TASKS_CAPABILITY, AIR_CUSTOM_INSTRUCTIONS_CAPABILITY, airCustomInstructions, AIR_DIFF_PATCH_CAPABILITY, AIR_PLAN_FILE_CAPABILITY, AIR_GOAL_KEY, AIR_KIND_KEY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, AIR_SKILL_PATH_KEY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
|
|
8
8
|
import * as sessionIndex from "./session-index/connection.js";
|
|
9
9
|
import { AsyncTaskRuntime, backgroundBashTaskFromToolResult, backgroundedBashToolCallIds, clientSupportsAsyncTasks, } from "./async-tasks.js";
|
|
@@ -16,7 +16,8 @@ import { promisify } from "node:util";
|
|
|
16
16
|
import packageJson from "../package.json" with { type: "json" };
|
|
17
17
|
import { applyAskElicitationResponse, askUserQuestionsToCreateRequest, createElicitationResponseToElicitResult, extractAskUserQuestions, extractRefusalFallbackPrompt, mcpElicitationToCreateRequest, REFUSAL_FALLBACK_DIALOG_KIND, refusalFallbackResultFromResponse, refusalFallbackToCreateRequest, } from "./elicitation.js";
|
|
18
18
|
import { forkSession } from "./fork-session.js";
|
|
19
|
-
import { subagentHistory } from "./subagent-history.js";
|
|
19
|
+
import { readSubagentMessages, subagentHistory } from "./subagent-history.js";
|
|
20
|
+
import { RfdSubagentReplay, replaySubagentTerminalState, } from "./subagent-replay.js";
|
|
20
21
|
import { readResumedSession, readResumedTail, } from "./resumed-session.js";
|
|
21
22
|
import { SessionTiming } from "./session-timing.js";
|
|
22
23
|
import { ALLOW_BYPASS } from "./permissions/modes.js";
|
|
@@ -32,14 +33,14 @@ import { ContextCompactionLifecycle, clientSupportsCompactionUpdates, contextCom
|
|
|
32
33
|
import { clientSupportsNotices, MAX_NOTICE_TITLE_LENGTH, normalizeNoticeText, noticeOrTranscriptUpdate, sentenceCase, splitNoticeText, } from "./session-notices.js";
|
|
33
34
|
import { applyTaskCreate, applyTaskList, applyTaskUpdate, clearHookCallbacks, completeHookCallback, createPostToolUseHook, hasHookCallback, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, changedTaskPlanEntries, forgetPublishedTaskPlan, unregisterHookCallback, } from "./tools.js";
|
|
34
35
|
import { previewPatchContent } from "./diff.js";
|
|
35
|
-
import { backgroundedBashToolCall } from "./tool-calls/background.js";
|
|
36
|
+
import { backgroundedBashToolCall, withRunningTerminal } from "./tool-calls/background.js";
|
|
36
37
|
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
|
|
37
38
|
import { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
|
|
38
39
|
import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
|
|
39
40
|
import { AcpToolCallRenderer, isFinalToolCallStatus, } from "./tool-calls/renderer.js";
|
|
40
41
|
import { resolveSkillPath } from "./tool-calls/reporters/interaction.js";
|
|
41
42
|
import { messageLoopYielder, nodeToWebReadable, nodeToWebWritable, Pushable, raceTimeoutAndAbort, unreachable, } from "./utils.js";
|
|
42
|
-
import { acceptedPlanToolResult, ExitPlanCoordinator, isExitPlanInterruptionResult, observeExitPlanToolResults, } from "./exit-plan.js";
|
|
43
|
+
import { acceptedPlanToolResult, approvedPlan, ExitPlanCoordinator, isExitPlanInterruptionResult, observeExitPlanToolResults, planModeUpdate, } from "./exit-plan.js";
|
|
43
44
|
import { DEFAULT_AGENT_ID } from "./session-config-ids.js";
|
|
44
45
|
import { parseToolResultMeta } from "./tool-result-meta.js";
|
|
45
46
|
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./usage-markdown.js";
|
|
@@ -280,6 +281,10 @@ const AUTONOMOUS_RESULT_ORIGINS = new Set([
|
|
|
280
281
|
"observer",
|
|
281
282
|
"observer-activity",
|
|
282
283
|
]);
|
|
284
|
+
/** The foreground work of a session (see ForegroundEvents): a prompt's turn,
|
|
285
|
+
* or a model cycle that Claude Code started on its own. At most one cycle
|
|
286
|
+
* runs at a time, so a constant names it. */
|
|
287
|
+
const AUTONOMOUS_CYCLE = "autonomous-cycle";
|
|
283
288
|
/** Whether this turn's terminal result arrived but its settlement is being
|
|
284
289
|
* held for background subagents it spawned (see Turn.deferredSettle). The
|
|
285
290
|
* single spelling of the hold predicate, shared by the consumer's settle
|
|
@@ -544,28 +549,6 @@ function subagentLaunchIds(content) {
|
|
|
544
549
|
? [block.id]
|
|
545
550
|
: []);
|
|
546
551
|
}
|
|
547
|
-
function replaySubagentTerminalState(block) {
|
|
548
|
-
if (block.is_error !== true)
|
|
549
|
-
return "completed";
|
|
550
|
-
const text = replayContentText(block.content).toLowerCase();
|
|
551
|
-
return /\b(?:cancelled|canceled|interrupted|stopped|killed)\b/.test(text)
|
|
552
|
-
? "cancelled"
|
|
553
|
-
: "failed";
|
|
554
|
-
}
|
|
555
|
-
function replayContentText(value, seen = new Set()) {
|
|
556
|
-
if (typeof value === "string")
|
|
557
|
-
return value;
|
|
558
|
-
if (typeof value !== "object" || value === null || seen.has(value))
|
|
559
|
-
return "";
|
|
560
|
-
seen.add(value);
|
|
561
|
-
if (Array.isArray(value))
|
|
562
|
-
return value.map((item) => replayContentText(item, seen)).join(" ");
|
|
563
|
-
const record = value;
|
|
564
|
-
return [record.text, record.content, record.message]
|
|
565
|
-
.map((item) => replayContentText(item, seen))
|
|
566
|
-
.filter(Boolean)
|
|
567
|
-
.join(" ");
|
|
568
|
-
}
|
|
569
552
|
function stripSubagentTextAndThinking(content) {
|
|
570
553
|
if (!Array.isArray(content))
|
|
571
554
|
return content;
|
|
@@ -1168,6 +1151,8 @@ export class ClaudeAcpAgent {
|
|
|
1168
1151
|
/** Last auth identity reported to the client, connection-scoped like
|
|
1169
1152
|
* `authenticate`/`logout`. Undefined means "not determined yet". */
|
|
1170
1153
|
currentAuthStatus;
|
|
1154
|
+
authStatusSettings;
|
|
1155
|
+
authStatusHelper;
|
|
1171
1156
|
/** In-flight `claude auth status --json` probe, shared by every caller so
|
|
1172
1157
|
* a concurrent `initialize` and start-of-prompt read never spawn two CLI
|
|
1173
1158
|
* processes. */
|
|
@@ -1194,6 +1179,15 @@ export class ClaudeAcpAgent {
|
|
|
1194
1179
|
* express: see {@link ToolCallClientCapabilities.v2}.
|
|
1195
1180
|
*/
|
|
1196
1181
|
v2;
|
|
1182
|
+
/** See the `foreground` constructor option. */
|
|
1183
|
+
foregroundEvents;
|
|
1184
|
+
/**
|
|
1185
|
+
* The foreground work of each session while it runs, by the session id that
|
|
1186
|
+
* the client knows. A clear-context restart replaces the Claude Code session
|
|
1187
|
+
* behind that id and moves the running turn to it, so the work belongs to the
|
|
1188
|
+
* id, not to a {@link Session}.
|
|
1189
|
+
*/
|
|
1190
|
+
foregroundWork = new Map();
|
|
1197
1191
|
/** The session index (docs/air-extensions.md, "Session index"). */
|
|
1198
1192
|
sessionIndex = new sessionIndex.SessionIndexConnection({
|
|
1199
1193
|
agent: this,
|
|
@@ -1203,6 +1197,7 @@ export class ClaudeAcpAgent {
|
|
|
1203
1197
|
});
|
|
1204
1198
|
constructor(client, logger, options = {}) {
|
|
1205
1199
|
this.v2 = options.v2 ?? false;
|
|
1200
|
+
this.foregroundEvents = options.foreground;
|
|
1206
1201
|
this.sessions = {};
|
|
1207
1202
|
this.client = new ChangedMetaClient(client, () => this.toolCallCapabilities.air.client);
|
|
1208
1203
|
this.logger = logger ?? console;
|
|
@@ -1218,6 +1213,7 @@ export class ClaudeAcpAgent {
|
|
|
1218
1213
|
},
|
|
1219
1214
|
applyFastMode: (session, enabled) => this.applyFastMode(session, enabled),
|
|
1220
1215
|
sessionUpdate: (notification) => this.client.sessionUpdate(notification),
|
|
1216
|
+
supportsPlanOperations: () => this.clientCapabilities?.plan != null,
|
|
1221
1217
|
ensureConsumer: (session, id) => this.ensureConsumer(session, id),
|
|
1222
1218
|
logError: (message, error) => this.logger.error(message, error),
|
|
1223
1219
|
destroyReplacement: (id, session) => {
|
|
@@ -1364,6 +1360,8 @@ export class ClaudeAcpAgent {
|
|
|
1364
1360
|
terminalAuthMethods.push(consoleLoginMethod);
|
|
1365
1361
|
}
|
|
1366
1362
|
}
|
|
1363
|
+
// The subagents RFD has only the client capability. AIR's draft also had
|
|
1364
|
+
// this agent capability, which AIR gets (`docs/air-extensions.md`).
|
|
1367
1365
|
const sessionCapabilities = {
|
|
1368
1366
|
additionalDirectories: {},
|
|
1369
1367
|
close: {},
|
|
@@ -1371,7 +1369,7 @@ export class ClaudeAcpAgent {
|
|
|
1371
1369
|
fork: {},
|
|
1372
1370
|
list: {},
|
|
1373
1371
|
resume: {},
|
|
1374
|
-
subagents: {},
|
|
1372
|
+
...(this.toolCallCapabilities.air.client ? { subagents: {} } : {}),
|
|
1375
1373
|
};
|
|
1376
1374
|
return {
|
|
1377
1375
|
protocolVersion: 1,
|
|
@@ -1492,14 +1490,30 @@ export class ClaudeAcpAgent {
|
|
|
1492
1490
|
configOptions: response.configOptions,
|
|
1493
1491
|
};
|
|
1494
1492
|
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Copies the session's transcript into a new session, then starts that
|
|
1495
|
+
* session, so the client can prompt it as soon as the response arrives, as
|
|
1496
|
+
* the schema requires of a fork. It starts like a resumed session: from the
|
|
1497
|
+
* copied transcript, with the model and the permission mode it records. A
|
|
1498
|
+
* later load or resume with the same settings reuses it.
|
|
1499
|
+
*/
|
|
1495
1500
|
async unstable_forkSession(params) {
|
|
1496
1501
|
if (this.providerUpdate)
|
|
1497
1502
|
await this.providerUpdate;
|
|
1498
|
-
|
|
1503
|
+
const { sessionId } = await forkSession(params, {
|
|
1499
1504
|
liveMessageIdToUuid: this.sessions[params.sessionId]?.messageIdToUuid,
|
|
1500
1505
|
logger: this.logger,
|
|
1501
1506
|
messageIdForGrouping,
|
|
1502
1507
|
});
|
|
1508
|
+
const { modes, configOptions } = await this.getOrCreateSession({
|
|
1509
|
+
sessionId,
|
|
1510
|
+
cwd: params.cwd,
|
|
1511
|
+
mcpServers: params.mcpServers,
|
|
1512
|
+
additionalDirectories: params.additionalDirectories,
|
|
1513
|
+
_meta: params._meta,
|
|
1514
|
+
});
|
|
1515
|
+
this.afterSetupResponse(sessionId, params.mcpServers ?? []);
|
|
1516
|
+
return { sessionId, modes, configOptions };
|
|
1503
1517
|
}
|
|
1504
1518
|
async resumeSession(params) {
|
|
1505
1519
|
if (this.providerUpdate)
|
|
@@ -1668,7 +1682,22 @@ export class ClaudeAcpAgent {
|
|
|
1668
1682
|
* under an apiKeyHelper) means "nothing to add", not "logged out" — keep
|
|
1669
1683
|
* what the CLI probe already established instead of overwriting it.
|
|
1670
1684
|
*/
|
|
1671
|
-
|
|
1685
|
+
useAuthStatusSettings(manager, settingsOverride, launchEnv) {
|
|
1686
|
+
const helper = manager.getApiKeyHelper(settingsOverride);
|
|
1687
|
+
const changed = manager.getCwd() !== (this.authStatusSettings?.manager.getCwd() ?? process.cwd()) ||
|
|
1688
|
+
helper !== this.authStatusHelper ||
|
|
1689
|
+
JSON.stringify(launchEnv) !== JSON.stringify(this.authStatusSettings?.env);
|
|
1690
|
+
this.authStatusSettings = { manager, override: settingsOverride, env: launchEnv };
|
|
1691
|
+
this.authStatusHelper = helper;
|
|
1692
|
+
if (changed) {
|
|
1693
|
+
// The epoch bump discards any probe that read the old context, and the
|
|
1694
|
+
// session account may carry no identity signal (an apiKeyHelper), so
|
|
1695
|
+
// only a probe in the new context can classify it.
|
|
1696
|
+
this.authEpoch += 1;
|
|
1697
|
+
void this.probeCliAuthStatus({ fresh: true });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
publishSessionAccountIdentity(account, apiKeyHelper) {
|
|
1672
1701
|
if (this.providerConfig) {
|
|
1673
1702
|
if (!this.loggedOverriddenAccount) {
|
|
1674
1703
|
this.loggedOverriddenAccount = true;
|
|
@@ -1679,7 +1708,7 @@ export class ClaudeAcpAgent {
|
|
|
1679
1708
|
if (this.gatewayAuthRequest) {
|
|
1680
1709
|
return;
|
|
1681
1710
|
}
|
|
1682
|
-
const fromSession = fromAccountInfo(account);
|
|
1711
|
+
const fromSession = fromAccountInfo(account, apiKeyHelper);
|
|
1683
1712
|
if (fromSession) {
|
|
1684
1713
|
this.setAuthStatus(fromSession);
|
|
1685
1714
|
}
|
|
@@ -1712,6 +1741,8 @@ export class ClaudeAcpAgent {
|
|
|
1712
1741
|
// auth-affecting event describes a world that no longer exists. Remember
|
|
1713
1742
|
// which one this read belongs to and drop the answer if it moved on.
|
|
1714
1743
|
const epoch = this.authEpoch;
|
|
1744
|
+
const context = this.authStatusSettings;
|
|
1745
|
+
const apiKeyHelper = context?.manager.getApiKeyHelper(context.override);
|
|
1715
1746
|
// ACP gateway auth bypasses the CLI credential store entirely, so the
|
|
1716
1747
|
// probe would report an identity that is not the one being used. A mere
|
|
1717
1748
|
// client provider override (`providers/set`) does NOT skip the probe: it
|
|
@@ -1723,8 +1754,13 @@ export class ClaudeAcpAgent {
|
|
|
1723
1754
|
let stdout;
|
|
1724
1755
|
try {
|
|
1725
1756
|
const cliPath = await claudeCliPath();
|
|
1726
|
-
|
|
1757
|
+
const args = ["auth", "status", "--json"];
|
|
1758
|
+
if (context?.override)
|
|
1759
|
+
args.push("--settings", JSON.stringify(context.override));
|
|
1760
|
+
({ stdout } = await execFileAsync(cliPath, args, {
|
|
1727
1761
|
timeout: AUTH_STATUS_PROBE_TIMEOUT_MS,
|
|
1762
|
+
cwd: context?.manager.getCwd() ?? process.cwd(),
|
|
1763
|
+
env: context?.env ? { ...process.env, ...context.env } : process.env,
|
|
1728
1764
|
}));
|
|
1729
1765
|
}
|
|
1730
1766
|
catch (error) {
|
|
@@ -1747,7 +1783,16 @@ export class ClaudeAcpAgent {
|
|
|
1747
1783
|
}
|
|
1748
1784
|
stdout = failed.stdout;
|
|
1749
1785
|
}
|
|
1750
|
-
|
|
1786
|
+
let status = fromCliStatus(stdout, apiKeyHelper);
|
|
1787
|
+
if (!context && status?.kind === "api_key" && status.detail === "API key helper") {
|
|
1788
|
+
try {
|
|
1789
|
+
const resolved = await resolveSettings({ cwd: process.cwd() });
|
|
1790
|
+
status = fromCliStatus(stdout, filterEscalatingDefaultMode(resolved).apiKeyHelper);
|
|
1791
|
+
}
|
|
1792
|
+
catch {
|
|
1793
|
+
this.logger.error("Could not resolve auth source settings");
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1751
1796
|
if (!status) {
|
|
1752
1797
|
this.logger.error("claude auth status returned unparseable output");
|
|
1753
1798
|
return undefined;
|
|
@@ -1987,13 +2032,7 @@ export class ClaudeAcpAgent {
|
|
|
1987
2032
|
async prompt(params) {
|
|
1988
2033
|
let events;
|
|
1989
2034
|
const outcome = new Promise((resolve, reject) => {
|
|
1990
|
-
events = {
|
|
1991
|
-
inserted() { },
|
|
1992
|
-
awaitingUser() { },
|
|
1993
|
-
resumed() { },
|
|
1994
|
-
ended: resolve,
|
|
1995
|
-
failed: reject,
|
|
1996
|
-
};
|
|
2035
|
+
events = { inserted() { }, ended: resolve, failed: reject };
|
|
1997
2036
|
});
|
|
1998
2037
|
await this.startTurn(params, events);
|
|
1999
2038
|
return outcome;
|
|
@@ -2019,6 +2058,7 @@ export class ClaudeAcpAgent {
|
|
|
2019
2058
|
// that finds the new credential must not be the one the refusal skipped.
|
|
2020
2059
|
// The guard itself consumes the result only at the next turn boundary (see
|
|
2021
2060
|
// `markSessionsWhoseAccountKindChanged`).
|
|
2061
|
+
this.useAuthStatusSettings(session.settingsManager, session.effortSettingsOverride, session.creationParams?._meta?.claudeCode?.options?.env);
|
|
2022
2062
|
void this.probeCliAuthStatus();
|
|
2023
2063
|
const signOutRespawn = this.respawnSignedOutSession(params.sessionId, session);
|
|
2024
2064
|
if (signOutRespawn)
|
|
@@ -2066,10 +2106,12 @@ export class ClaudeAcpAgent {
|
|
|
2066
2106
|
completeTurn = resolve;
|
|
2067
2107
|
}),
|
|
2068
2108
|
resolve: (outcome) => {
|
|
2109
|
+
this.endForeground(params.sessionId, turn, { outcome });
|
|
2069
2110
|
events.ended(outcome);
|
|
2070
2111
|
completeTurn();
|
|
2071
2112
|
},
|
|
2072
2113
|
reject: (error) => {
|
|
2114
|
+
this.endForeground(params.sessionId, turn, { error });
|
|
2073
2115
|
events.failed(error);
|
|
2074
2116
|
completeTurn();
|
|
2075
2117
|
},
|
|
@@ -2235,7 +2277,7 @@ export class ClaudeAcpAgent {
|
|
|
2235
2277
|
logger: this.logger,
|
|
2236
2278
|
// The guard reads the account anyway; reuse that read to keep the
|
|
2237
2279
|
// reported identity current, refusal or not.
|
|
2238
|
-
onAccount: (account) => this.publishSessionAccountIdentity(account),
|
|
2280
|
+
onAccount: (account) => this.publishSessionAccountIdentity(account, session.settingsManager.getApiKeyHelper(session.effortSettingsOverride)),
|
|
2239
2281
|
});
|
|
2240
2282
|
}
|
|
2241
2283
|
async goal(params) {
|
|
@@ -2387,7 +2429,7 @@ export class ClaudeAcpAgent {
|
|
|
2387
2429
|
// Deliver into the running turn rather than queuing behind it as a fresh
|
|
2388
2430
|
// prompt would.
|
|
2389
2431
|
userMessage.priority =
|
|
2390
|
-
(session.
|
|
2432
|
+
(session.openUserInputs?.size ?? 0) > 0 ? STEER_PRIORITY_LATER : STEER_PRIORITY_NOW;
|
|
2391
2433
|
// Mark before the push and in the same synchronous section as the in-flight
|
|
2392
2434
|
// check: the interrupt can have the CLI finalizing the aborted cycle by the
|
|
2393
2435
|
// time the consumer next runs, and an unmarked result would settle the turn
|
|
@@ -2400,6 +2442,8 @@ export class ClaudeAcpAgent {
|
|
|
2400
2442
|
if (turnInFlight.deferredSettle !== undefined) {
|
|
2401
2443
|
turnInFlight.steeredSettle = turnInFlight.deferredSettle;
|
|
2402
2444
|
turnInFlight.deferredSettle = undefined;
|
|
2445
|
+
// The held turn's work had ended at its result; the steer resumes it.
|
|
2446
|
+
this.startForeground(sessionId, turnInFlight);
|
|
2403
2447
|
}
|
|
2404
2448
|
session.input.push(userMessage);
|
|
2405
2449
|
const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
|
|
@@ -2488,6 +2532,10 @@ export class ClaudeAcpAgent {
|
|
|
2488
2532
|
streamedBlocksByParent.set(key, (blocks = []));
|
|
2489
2533
|
return blocks;
|
|
2490
2534
|
};
|
|
2535
|
+
// The streams, keyed like streamedBlocksByParent, that dropped a delta
|
|
2536
|
+
// after a cancel since their record last reset, so the record no longer
|
|
2537
|
+
// matches what the client got (see the stream_event case).
|
|
2538
|
+
const droppedStreams = new Set();
|
|
2491
2539
|
// A client gets the consolidated subagent text when it negotiated the
|
|
2492
2540
|
// transcript extension or the `forwardSubagentText` session option. AIR
|
|
2493
2541
|
// also gets it with native subagent sessions, and AIR gets no streamed
|
|
@@ -2516,7 +2564,7 @@ export class ClaudeAcpAgent {
|
|
|
2516
2564
|
* recognizable by the `parentToolUseId` meta that toAcpNotifications
|
|
2517
2565
|
* stamps from `parent_tool_use_id`, and never reach the top-level feed
|
|
2518
2566
|
* as the turn's answer. */
|
|
2519
|
-
const subagents = (session.nativeSubagentRuntime ??= new NativeSubagentRuntime(
|
|
2567
|
+
const subagents = (session.nativeSubagentRuntime ??= new NativeSubagentRuntime(subagentForm(this.clientCapabilities), params.sessionId, session, async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), this.logger));
|
|
2520
2568
|
// Adapter-composed advisories (hook feedback, model fallbacks) are live
|
|
2521
2569
|
// events, not something the model said. Clients on the notice contract get
|
|
2522
2570
|
// them as `notice` updates; the rest keep the bold-label transcript line.
|
|
@@ -2551,6 +2599,19 @@ export class ClaudeAcpAgent {
|
|
|
2551
2599
|
const eagerOwnerSessionId = toolCallId
|
|
2552
2600
|
? session.eagerToolCallSessions?.get(toolCallId)
|
|
2553
2601
|
: undefined;
|
|
2602
|
+
// Before routing, which can buffer or drop the update: a cancel still has
|
|
2603
|
+
// to know the parent of a call that is open here. A later update can be
|
|
2604
|
+
// the first to name it, for a call that a permission request emitted.
|
|
2605
|
+
if (toolCallId &&
|
|
2606
|
+
update.sessionUpdate === "tool_call_update" &&
|
|
2607
|
+
isFinalToolCallStatus(update.status)) {
|
|
2608
|
+
session.toolCallParents?.delete(toolCallId);
|
|
2609
|
+
}
|
|
2610
|
+
else if (toolCallId &&
|
|
2611
|
+
claudeMeta?.parentToolUseId &&
|
|
2612
|
+
session.emittedToolCalls.has(toolCallId)) {
|
|
2613
|
+
recordToolCallParent(session, toolCallId, claudeMeta.parentToolUseId);
|
|
2614
|
+
}
|
|
2554
2615
|
const routedNotification = await subagents.route(notification, sendUpdate, eagerOwnerSessionId);
|
|
2555
2616
|
if (!routedNotification) {
|
|
2556
2617
|
// Native Agent/Task control calls are intentionally not transcript
|
|
@@ -2706,6 +2767,7 @@ export class ClaudeAcpAgent {
|
|
|
2706
2767
|
const activateTurn = (turn) => {
|
|
2707
2768
|
session.activeTurn = turn;
|
|
2708
2769
|
session.cancelled = false;
|
|
2770
|
+
session.cancelledPrompts = undefined;
|
|
2709
2771
|
compaction.resume();
|
|
2710
2772
|
if (turn.localCommand?.startsAtActivation)
|
|
2711
2773
|
ensureLocalCommandMarkdown(turn);
|
|
@@ -2742,7 +2804,7 @@ export class ClaudeAcpAgent {
|
|
|
2742
2804
|
reportInserted(turn);
|
|
2743
2805
|
// A request can already be open: Claude Code asks for a queued prompt
|
|
2744
2806
|
// before the consumer reaches its echo.
|
|
2745
|
-
this.syncAwaitingUser(session);
|
|
2807
|
+
this.syncAwaitingUser(params.sessionId, session);
|
|
2746
2808
|
};
|
|
2747
2809
|
/** Report that Claude Code took the turn's prompt in, once. A turn can be
|
|
2748
2810
|
* settled by now: an echo hand-off awaits settling the previous turn, and
|
|
@@ -2752,6 +2814,7 @@ export class ClaudeAcpAgent {
|
|
|
2752
2814
|
return;
|
|
2753
2815
|
turn.insertedReported = true;
|
|
2754
2816
|
turn.events.inserted(turn.promptUuid);
|
|
2817
|
+
this.startForeground(params.sessionId, turn);
|
|
2755
2818
|
};
|
|
2756
2819
|
/** Ensure there is an active turn before a user-turn result that carries no
|
|
2757
2820
|
* echo to activate it, by promoting the queue head. Most turns are
|
|
@@ -2776,7 +2839,7 @@ export class ClaudeAcpAgent {
|
|
|
2776
2839
|
* a stamp naming an orphaned command consumes the result outright, and a
|
|
2777
2840
|
* stamp naming anything else positively refutes "this is a dead turn's
|
|
2778
2841
|
* result", so the dup-over-loss one-skip must not eat it. */
|
|
2779
|
-
const ensureActiveTurn = async (resultUserMessageUuid) => {
|
|
2842
|
+
const ensureActiveTurn = async (resultUserMessageUuid, resultUserMessageUuids) => {
|
|
2780
2843
|
if (session.activeTurn) {
|
|
2781
2844
|
if (!isHeldOpen(session.activeTurn)) {
|
|
2782
2845
|
return;
|
|
@@ -2802,8 +2865,8 @@ export class ClaudeAcpAgent {
|
|
|
2802
2865
|
}
|
|
2803
2866
|
// Orphan accounting runs BEFORE the head check: an orphan's echo-less
|
|
2804
2867
|
// result can arrive with an EMPTY queue (the common post-cancel
|
|
2805
|
-
// timeline — the
|
|
2806
|
-
// user hasn't typed yet), and it must still be consumed here. Skipping
|
|
2868
|
+
// timeline — the cancelled turn settled at its interrupted result and
|
|
2869
|
+
// the user hasn't typed yet), and it must still be consumed here. Skipping
|
|
2807
2870
|
// the bookkeeping when there is nothing to promote would leave a
|
|
2808
2871
|
// phantom entry/count that swallows the next live echo-less result
|
|
2809
2872
|
// (e.g. /compact) instead.
|
|
@@ -2872,6 +2935,14 @@ export class ClaudeAcpAgent {
|
|
|
2872
2935
|
}
|
|
2873
2936
|
}
|
|
2874
2937
|
}
|
|
2938
|
+
// A result that names a prompt a cancel stopped is that prompt's, even
|
|
2939
|
+
// when the orphan accounting above lost track of it: the interrupt
|
|
2940
|
+
// receipt leaves out a prompt that Claude Code had already taken off its
|
|
2941
|
+
// queue. It must not settle the prompt sent after the cancel.
|
|
2942
|
+
const stamps = resultUserMessageUuids ??
|
|
2943
|
+
(resultUserMessageUuid !== undefined ? [resultUserMessageUuid] : []);
|
|
2944
|
+
if (stamps.some((uuid) => session.cancelledPrompts?.has(uuid)))
|
|
2945
|
+
return;
|
|
2875
2946
|
const head = firstUnsettledQueuedTurn();
|
|
2876
2947
|
if (!head) {
|
|
2877
2948
|
return;
|
|
@@ -2971,17 +3042,53 @@ export class ClaudeAcpAgent {
|
|
|
2971
3042
|
};
|
|
2972
3043
|
/** Registers a live background task (see `liveBackgroundTasks`). A
|
|
2973
3044
|
* subagent is also recorded on the active turn, so that turn holds until
|
|
2974
|
-
* the subagent settles (see `Turn.spawnedTaskIds`).
|
|
2975
|
-
|
|
2976
|
-
|
|
3045
|
+
* the subagent settles (see `Turn.spawnedTaskIds`). Its requests may
|
|
3046
|
+
* already be open, and where it runs decides whether they block the
|
|
3047
|
+
* foreground work, so that is checked again. */
|
|
3048
|
+
const registerLiveTask = (taskId, parentToolUseId, isSubagent, placement = {}) => {
|
|
3049
|
+
session.liveBackgroundTasks.set(taskId, {
|
|
3050
|
+
parentToolUseId,
|
|
3051
|
+
isSubagent,
|
|
3052
|
+
...(placement.backgrounded !== undefined ? { backgrounded: placement.backgrounded } : {}),
|
|
3053
|
+
...(placement.parentTaskId !== undefined ? { parentTaskId: placement.parentTaskId } : {}),
|
|
3054
|
+
});
|
|
2977
3055
|
if (parentToolUseId)
|
|
2978
3056
|
forgetForegroundToolCall(session, parentToolUseId);
|
|
2979
3057
|
session.resumableSubagents?.delete(taskId);
|
|
2980
3058
|
if (isSubagent && session.activeTurn && !session.activeTurn.settled) {
|
|
2981
3059
|
(session.activeTurn.spawnedTaskIds ??= new Set()).add(taskId);
|
|
2982
3060
|
}
|
|
3061
|
+
this.syncAwaitingUser(params.sessionId, session);
|
|
3062
|
+
};
|
|
3063
|
+
/** Ends the display terminal of a command that went to the background, once
|
|
3064
|
+
* its task ends (see `Session.backgroundTerminals`). Claude Code reports no
|
|
3065
|
+
* exit code for it, and `completed` does not mean 0: Claude Code accepts
|
|
3066
|
+
* some non-zero codes as a success, such as grep's 1. */
|
|
3067
|
+
const endBackgroundTerminal = async (taskId) => {
|
|
3068
|
+
const terminal = session.backgroundTerminals?.get(taskId);
|
|
3069
|
+
if (!terminal)
|
|
3070
|
+
return;
|
|
3071
|
+
session.backgroundTerminals.delete(taskId);
|
|
3072
|
+
await sendUpdate({
|
|
3073
|
+
sessionId: params.sessionId,
|
|
3074
|
+
update: {
|
|
3075
|
+
sessionUpdate: "tool_call_update",
|
|
3076
|
+
toolCallId: terminal.toolCallId,
|
|
3077
|
+
_meta: {
|
|
3078
|
+
...(terminal.parentToolUseId
|
|
3079
|
+
? { claudeCode: { parentToolUseId: terminal.parentToolUseId } }
|
|
3080
|
+
: {}),
|
|
3081
|
+
terminal_exit: { terminal_id: terminal.toolCallId, exit_code: null, signal: null },
|
|
3082
|
+
},
|
|
3083
|
+
},
|
|
3084
|
+
});
|
|
3085
|
+
};
|
|
3086
|
+
/** Settles a task. Its request may still be open, and an agent without a
|
|
3087
|
+
* record no longer blocks the foreground work, so that is checked again. */
|
|
3088
|
+
const settleLiveTask = (taskId) => {
|
|
3089
|
+
settleLiveBackgroundTask(session, taskId);
|
|
3090
|
+
this.syncAwaitingUser(params.sessionId, session);
|
|
2983
3091
|
};
|
|
2984
|
-
const settleLiveTask = (taskId) => settleLiveBackgroundTask(session, taskId);
|
|
2985
3092
|
/** Registers a settled subagent again when the SDK resumes it without a
|
|
2986
3093
|
* new `task_started`. The active turn is the one that resumed it, so it
|
|
2987
3094
|
* holds until the subagent settles again. A repeated resume signal finds
|
|
@@ -2990,8 +3097,9 @@ export class ClaudeAcpAgent {
|
|
|
2990
3097
|
if (session.liveBackgroundTasks.has(taskId))
|
|
2991
3098
|
return;
|
|
2992
3099
|
const settled = session.resumableSubagents?.get(taskId);
|
|
3100
|
+
// Claude Code always resumes a subagent in the background.
|
|
2993
3101
|
if (settled)
|
|
2994
|
-
registerLiveTask(taskId, settled.parentToolUseId, true);
|
|
3102
|
+
registerLiveTask(taskId, settled.parentToolUseId, true, { backgrounded: true });
|
|
2995
3103
|
};
|
|
2996
3104
|
/** Whether any background subagent this turn spawned is still live —
|
|
2997
3105
|
* while true, the turn's settlement stays deferred so the subagent's
|
|
@@ -3044,11 +3152,100 @@ export class ClaudeAcpAgent {
|
|
|
3044
3152
|
!session.activeTurn.settled &&
|
|
3045
3153
|
turnAwaitingSubagents(session.activeTurn)) {
|
|
3046
3154
|
session.activeTurn.deferredSettle = outcome;
|
|
3155
|
+
// The hold delays v1's answer; the turn's work ended at its result.
|
|
3156
|
+
this.endForeground(params.sessionId, session.activeTurn, { outcome });
|
|
3047
3157
|
}
|
|
3048
3158
|
else {
|
|
3049
3159
|
await settleActive(outcome);
|
|
3050
3160
|
}
|
|
3051
3161
|
};
|
|
3162
|
+
/** Sends `report`, which ends the tool call `toolCallId` without its result
|
|
3163
|
+
* (see `AcpToolCallRenderer.unfinished`), and forgets the call. Forgetting
|
|
3164
|
+
* comes first, so that a late hook or a second close cannot overwrite the
|
|
3165
|
+
* report. */
|
|
3166
|
+
const closeToolCall = async (toolCallId, report) => {
|
|
3167
|
+
unregisterHookCallback(toolCallId);
|
|
3168
|
+
session.emittedToolCalls.delete(toolCallId);
|
|
3169
|
+
delete session.toolUseCache[toolCallId];
|
|
3170
|
+
session.toolCallFields?.delete(toolCallId);
|
|
3171
|
+
session.dispatchedToolCalls?.delete(toolCallId);
|
|
3172
|
+
session.toolCallParents?.delete(toolCallId);
|
|
3173
|
+
await sendUpdate({ sessionId: params.sessionId, update: report });
|
|
3174
|
+
};
|
|
3175
|
+
/** Whether the subagent that the Agent call `agentToolUseId` started runs in
|
|
3176
|
+
* the background and has not ended. */
|
|
3177
|
+
const runsInBackground = (agentToolUseId) => [...session.liveBackgroundTasks.values()].some((task) => task.isSubagent &&
|
|
3178
|
+
task.backgrounded === true &&
|
|
3179
|
+
!task.endedPerLevel &&
|
|
3180
|
+
task.parentToolUseId === agentToolUseId);
|
|
3181
|
+
/** Closes the tool calls that the work a cancel stopped left open, as that
|
|
3182
|
+
* work ends. Claude Code's interrupt ends each running tool of the main
|
|
3183
|
+
* conversation with an error result, which `sendInterruptedToolResults`
|
|
3184
|
+
* delivers. What is still open here has no result to deliver:
|
|
3185
|
+
* - a tool of a subagent: Claude Code writes its interrupted result only to
|
|
3186
|
+
* the subagent's own transcript, not to the stream;
|
|
3187
|
+
* - a tool call that Claude abandoned before it ran;
|
|
3188
|
+
* - every tool call, when the stream gives no results at all, as when the
|
|
3189
|
+
* force-cancel backstop ends a wedged turn.
|
|
3190
|
+
* A foreground subagent ends with the work that waits on it. A background
|
|
3191
|
+
* subagent keeps its tool calls open until the SDK reports its end, which
|
|
3192
|
+
* Claude Code 2.1.293 does for the ones it stops, before the turn's result
|
|
3193
|
+
* (see `NativeSubagentRuntime.parentCancelled`).
|
|
3194
|
+
* Returns false when the turn ended meanwhile. */
|
|
3195
|
+
const closeCancelledToolCalls = async (turn) => {
|
|
3196
|
+
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
|
3197
|
+
for (const toolCallId of [...session.emittedToolCalls]) {
|
|
3198
|
+
const parentToolUseId = session.toolCallParents?.get(toolCallId);
|
|
3199
|
+
if (parentToolUseId !== undefined && runsInBackground(parentToolUseId))
|
|
3200
|
+
continue;
|
|
3201
|
+
const report = renderer.unfinished({
|
|
3202
|
+
toolCallId,
|
|
3203
|
+
ran: session.dispatchedToolCalls?.has(toolCallId) === true,
|
|
3204
|
+
cancelled: true,
|
|
3205
|
+
});
|
|
3206
|
+
// Named as the call's other updates are: the subagent runtime routes the
|
|
3207
|
+
// report with them, to the child session, or to none that has ended.
|
|
3208
|
+
if (parentToolUseId !== undefined)
|
|
3209
|
+
stampParentToolUseId(report, parentToolUseId);
|
|
3210
|
+
await closeToolCall(toolCallId, report);
|
|
3211
|
+
if (turn.settled || session.activeTurn !== turn)
|
|
3212
|
+
return false;
|
|
3213
|
+
}
|
|
3214
|
+
return true;
|
|
3215
|
+
};
|
|
3216
|
+
/** Delivers the results that the work a cancel stopped sends for tool calls
|
|
3217
|
+
* that the client holds open. Claude Code's interrupt ends each running tool
|
|
3218
|
+
* of the main conversation with an error result, before the result that
|
|
3219
|
+
* ends the turn. The tool call gets that result like any other, so its
|
|
3220
|
+
* final status and `nonExecutionKind` reach the client. Nothing else of the
|
|
3221
|
+
* message is applied: the other effects of a tool result (async tasks, plan
|
|
3222
|
+
* state, the ExitPlanMode flow) belong to the stopped work, which stays
|
|
3223
|
+
* dropped. */
|
|
3224
|
+
const sendInterruptedToolResults = async (message) => {
|
|
3225
|
+
const content = message.message.content;
|
|
3226
|
+
if (!Array.isArray(content))
|
|
3227
|
+
return;
|
|
3228
|
+
const results = content.filter((block) => block.type === "tool_result" && session.emittedToolCalls.has(block.tool_use_id));
|
|
3229
|
+
if (results.length === 0)
|
|
3230
|
+
return;
|
|
3231
|
+
const resultCount = content.filter((block) => block.type === "tool_result").length;
|
|
3232
|
+
for (const notification of toAcpNotifications(results, "user", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
3233
|
+
clientCapabilities: this.clientCapabilities,
|
|
3234
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
3235
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
3236
|
+
cwd: session.cwd,
|
|
3237
|
+
taskState: session.taskState,
|
|
3238
|
+
emittedToolCalls: session.emittedToolCalls,
|
|
3239
|
+
toolCallFields: toolCallFieldsOf(session),
|
|
3240
|
+
messageId: messageIdForGrouping(message),
|
|
3241
|
+
// `tool_use_result` describes the one result of its message.
|
|
3242
|
+
toolUseResult: results.length === resultCount ? message.tool_use_result : undefined,
|
|
3243
|
+
toolResultMeta: message.tool_result_meta,
|
|
3244
|
+
cancelled: true,
|
|
3245
|
+
})) {
|
|
3246
|
+
await sendUpdate(notification);
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3052
3249
|
/** At the actual turn boundary, preview its checkpoint, settle the active
|
|
3053
3250
|
* turn exactly once, disarm the force-cancel backstop, and drop it from
|
|
3054
3251
|
* the queue. Cancellation and provider failures skip checkpoint I/O. */
|
|
@@ -3074,16 +3271,7 @@ export class ClaudeAcpAgent {
|
|
|
3074
3271
|
* ended meanwhile.
|
|
3075
3272
|
*/
|
|
3076
3273
|
const closeUnfinished = async (toolCallId, ran) => {
|
|
3077
|
-
|
|
3078
|
-
unregisterHookCallback(toolCallId);
|
|
3079
|
-
session.emittedToolCalls.delete(toolCallId);
|
|
3080
|
-
delete session.toolUseCache[toolCallId];
|
|
3081
|
-
session.toolCallFields?.delete(toolCallId);
|
|
3082
|
-
session.dispatchedToolCalls?.delete(toolCallId);
|
|
3083
|
-
await sendUpdate({
|
|
3084
|
-
sessionId: params.sessionId,
|
|
3085
|
-
update: renderer.unfinished({ toolCallId, ran }),
|
|
3086
|
-
});
|
|
3274
|
+
await closeToolCall(toolCallId, renderer.unfinished({ toolCallId, ran }));
|
|
3087
3275
|
// Cancellation can arrive while we await the client update.
|
|
3088
3276
|
if (turn.settled || session.activeTurn !== turn)
|
|
3089
3277
|
return false;
|
|
@@ -3116,6 +3304,13 @@ export class ClaudeAcpAgent {
|
|
|
3116
3304
|
return;
|
|
3117
3305
|
}
|
|
3118
3306
|
}
|
|
3307
|
+
// The client is told that the work a cancel stopped ended (the prompt
|
|
3308
|
+
// response on v1, `idle` on v2) only after every tool call that work
|
|
3309
|
+
// opened has ended.
|
|
3310
|
+
if (result.stopReason === "cancelled" && session.cancelled) {
|
|
3311
|
+
if (!(await closeCancelledToolCalls(turn)))
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3119
3314
|
turn.settling = true;
|
|
3120
3315
|
turn.settlingOutcome = result;
|
|
3121
3316
|
if (reportReason === "notReported") {
|
|
@@ -3220,6 +3415,7 @@ export class ClaudeAcpAgent {
|
|
|
3220
3415
|
/** Reject every in-flight turn — used when the stream dies. */
|
|
3221
3416
|
const failAllTurns = (error) => {
|
|
3222
3417
|
disarmForceCancel(session);
|
|
3418
|
+
this.endAutonomousCycle(params.sessionId, { error });
|
|
3223
3419
|
const turns = session.activeTurn
|
|
3224
3420
|
? [session.activeTurn, ...(session.turnQueue ?? []).filter((t) => t !== session.activeTurn)]
|
|
3225
3421
|
: [...(session.turnQueue ?? [])];
|
|
@@ -3317,9 +3513,9 @@ export class ClaudeAcpAgent {
|
|
|
3317
3513
|
}
|
|
3318
3514
|
}
|
|
3319
3515
|
else if (active.commandResultSeen) {
|
|
3320
|
-
// Its result was already consumed (
|
|
3321
|
-
//
|
|
3322
|
-
//
|
|
3516
|
+
// Its result was already consumed (at the cancelled guard);
|
|
3517
|
+
// only the terminal frame is outstanding, which no-ops with no
|
|
3518
|
+
// entry. Nothing to track.
|
|
3323
3519
|
}
|
|
3324
3520
|
else {
|
|
3325
3521
|
// The wedged turn WAS dispatched (it's active), so track it
|
|
@@ -3386,6 +3582,9 @@ export class ClaudeAcpAgent {
|
|
|
3386
3582
|
// it), but the stored one is the authoritative source.
|
|
3387
3583
|
const inFlight = session.activeTurn;
|
|
3388
3584
|
await finishLifecycle(session.cancelled ? "cancelled" : "failed", session.cancelled ? "stopped" : "failed", "at end of stream");
|
|
3585
|
+
this.endAutonomousCycle(params.sessionId, session.cancelled
|
|
3586
|
+
? { outcome: { stopReason: "cancelled" } }
|
|
3587
|
+
: { error: RequestError.internalError(undefined, SESSION_ENDED_MESSAGE) });
|
|
3389
3588
|
await settleActive(session.cancelled
|
|
3390
3589
|
? cancelledOutcome(session, inFlight)
|
|
3391
3590
|
: (inFlight?.deferredSettle ?? turnOutcome(session, stopReason)));
|
|
@@ -3528,6 +3727,7 @@ export class ClaudeAcpAgent {
|
|
|
3528
3727
|
await this.publishRuntimeGoal(params.sessionId, toGoalSnapshot(activeGoal));
|
|
3529
3728
|
continue;
|
|
3530
3729
|
}
|
|
3730
|
+
this.startAutonomousCycle(params.sessionId, session, message);
|
|
3531
3731
|
switch (message.type) {
|
|
3532
3732
|
case "system":
|
|
3533
3733
|
switch (message.subtype) {
|
|
@@ -3539,6 +3739,9 @@ export class ClaudeAcpAgent {
|
|
|
3539
3739
|
if (message.capabilities?.includes("msg_lifecycle_v1")) {
|
|
3540
3740
|
session.msgLifecycleV1 = true;
|
|
3541
3741
|
}
|
|
3742
|
+
if (message.capabilities?.includes("request_marker_lists_v1")) {
|
|
3743
|
+
session.requestMarkerListsV1 = true;
|
|
3744
|
+
}
|
|
3542
3745
|
// A fresh `system`/init (e.g. after reinitialize) can carry an
|
|
3543
3746
|
// updated Fast mode state; reconcile it with what we seeded at
|
|
3544
3747
|
// session creation.
|
|
@@ -3574,6 +3777,24 @@ export class ClaudeAcpAgent {
|
|
|
3574
3777
|
}
|
|
3575
3778
|
break;
|
|
3576
3779
|
case "status": {
|
|
3780
|
+
// A model request that serves none of the prompts a cancel
|
|
3781
|
+
// stopped is new work, such as Claude Code's follow-up after a
|
|
3782
|
+
// background command finished. It comes after the stopped
|
|
3783
|
+
// work's result: Claude Code ends the interrupted turn first,
|
|
3784
|
+
// then runs any prompt it had already taken off its queue (its
|
|
3785
|
+
// request names it, so the cancel still drops it), then its own
|
|
3786
|
+
// follow-ups, whose requests name no prompt.
|
|
3787
|
+
if (message.status === "requesting" &&
|
|
3788
|
+
session.cancelled &&
|
|
3789
|
+
session.requestMarkerListsV1 &&
|
|
3790
|
+
!servesCancelledWork(session, message)) {
|
|
3791
|
+
session.cancelled = false;
|
|
3792
|
+
session.cancelledPrompts = undefined;
|
|
3793
|
+
// The stopped work's tail has drained, as at activation.
|
|
3794
|
+
compaction.resume();
|
|
3795
|
+
}
|
|
3796
|
+
if (message.status === "requesting")
|
|
3797
|
+
session.modelRequestSinceResult = true;
|
|
3577
3798
|
if (message.status === "compacting") {
|
|
3578
3799
|
await compaction.start(message.uuid);
|
|
3579
3800
|
}
|
|
@@ -3665,6 +3886,17 @@ export class ClaudeAcpAgent {
|
|
|
3665
3886
|
session.owedTrailingIdles = 0;
|
|
3666
3887
|
}
|
|
3667
3888
|
if (message.state === "idle") {
|
|
3889
|
+
// A model cycle that Claude Code started on its own normally
|
|
3890
|
+
// ends at its result. An idle that no earlier result owes
|
|
3891
|
+
// means that result will not come (the stream dropped
|
|
3892
|
+
// mid-cycle, as for a turn in issue #825), so the cycle ends
|
|
3893
|
+
// here. An owed idle belongs to an earlier result: it can lag
|
|
3894
|
+
// past the cycle's first output.
|
|
3895
|
+
if (session.owedTrailingIdles === 0) {
|
|
3896
|
+
this.endAutonomousCycle(params.sessionId, {
|
|
3897
|
+
outcome: { stopReason: session.cancelled ? "cancelled" : "end_turn" },
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3668
3900
|
// A non-cancelled turn normally settled at its terminal
|
|
3669
3901
|
// `result` already (issue #773), and that result recorded an
|
|
3670
3902
|
// owed trailing idle — absorbed here via the decrement. We
|
|
@@ -3674,9 +3906,9 @@ export class ClaudeAcpAgent {
|
|
|
3674
3906
|
// the time it arrives the SDK may have echoed the NEXT turn
|
|
3675
3907
|
// and activated it — settling now would resolve that new
|
|
3676
3908
|
// turn prematurely with end_turn and ~zero usage, dropping
|
|
3677
|
-
// its real result. A cancelled turn
|
|
3678
|
-
//
|
|
3679
|
-
//
|
|
3909
|
+
// its real result. A cancelled turn settles at its
|
|
3910
|
+
// interrupted result (at the `session.cancelled` guard); one
|
|
3911
|
+
// whose result never came settles here.
|
|
3680
3912
|
//
|
|
3681
3913
|
// An idle that is NOT owed while the active turn is still
|
|
3682
3914
|
// unsettled is the issue #825 signature: `idle` is the SDK's
|
|
@@ -3688,12 +3920,9 @@ export class ClaudeAcpAgent {
|
|
|
3688
3920
|
// the turn NOW so its session/prompt gets a terminal
|
|
3689
3921
|
// response, instead of leaving it hanging until the next
|
|
3690
3922
|
// prompt drains the wreckage.
|
|
3691
|
-
// A cancelled turn
|
|
3692
|
-
//
|
|
3693
|
-
//
|
|
3694
|
-
// report it — clients metering spend would otherwise lose
|
|
3695
|
-
// the interrupted turn's tokens entirely (issue #844). Zero
|
|
3696
|
-
// when the cancel pre-empted the result (wedge/force-cancel).
|
|
3923
|
+
// A cancelled turn reaches this with no result of its own,
|
|
3924
|
+
// so its usage is zero (wedge/force-cancel), or, for a held
|
|
3925
|
+
// turn, the usage its result recorded (issue #844).
|
|
3697
3926
|
if (session.cancelled && session.activeTurn && !session.activeTurn.settled) {
|
|
3698
3927
|
// A held turn's result passed long ago; this idle is the
|
|
3699
3928
|
// trailer of the followup cycle the cancel interrupted,
|
|
@@ -3746,8 +3975,11 @@ export class ClaudeAcpAgent {
|
|
|
3746
3975
|
// Via the subagent gate, not settleActive: a steered turn
|
|
3747
3976
|
// can also have spawned background subagents, which own it
|
|
3748
3977
|
// from here (settles now if none is live, holds otherwise).
|
|
3978
|
+
const outcome = session.activeTurn.steeredSettle;
|
|
3749
3979
|
const steered = session.activeTurn;
|
|
3750
|
-
steered.deferredSettle =
|
|
3980
|
+
steered.deferredSettle = outcome;
|
|
3981
|
+
// A hold delays v1's answer; the turn's work ends here.
|
|
3982
|
+
this.endForeground(params.sessionId, steered, { outcome });
|
|
3751
3983
|
leaveSteerLane(steered);
|
|
3752
3984
|
await settleDeferredIfDrained();
|
|
3753
3985
|
}
|
|
@@ -3962,7 +4194,10 @@ export class ClaudeAcpAgent {
|
|
|
3962
4194
|
// turn: a turn only ever waits on its own subagents, and a
|
|
3963
4195
|
// spawn during a held-open drain window (an agent chain)
|
|
3964
4196
|
// extends that turn's hold.
|
|
3965
|
-
registerLiveTask(message.task_id, message.tool_use_id, !!message.subagent_type
|
|
4197
|
+
registerLiveTask(message.task_id, message.tool_use_id, !!message.subagent_type, {
|
|
4198
|
+
backgrounded: message.is_backgrounded,
|
|
4199
|
+
parentTaskId: message.parent_task_id,
|
|
4200
|
+
});
|
|
3966
4201
|
await subagents.taskStarted({
|
|
3967
4202
|
taskId: message.task_id,
|
|
3968
4203
|
toolUseId: message.tool_use_id,
|
|
@@ -3986,9 +4221,14 @@ export class ClaudeAcpAgent {
|
|
|
3986
4221
|
this.logger.log(`Ignoring task_notification for task ${message.task_id} from earlier run ${message.run_id}`);
|
|
3987
4222
|
break;
|
|
3988
4223
|
}
|
|
4224
|
+
// In the RFD form, a resumed child runs under the call that
|
|
4225
|
+
// resumed it, and the end of an earlier delegation ends
|
|
4226
|
+
// nothing: the child and its live task belong to the new run.
|
|
4227
|
+
if (subagents.isStaleEnd(message.task_id, message.tool_use_id))
|
|
4228
|
+
break;
|
|
3989
4229
|
// The task settled — no further tool calls can originate
|
|
3990
4230
|
// from it, so its registry entry can be dropped.
|
|
3991
|
-
await subagents.finishTask(message.task_id, message.status, sendUpdate, message.tool_use_id);
|
|
4231
|
+
await subagents.finishTask(message.task_id, message.status, sendUpdate, message.tool_use_id, { reason: message.reason });
|
|
3992
4232
|
await asyncTasks.taskNotification({
|
|
3993
4233
|
task_id: message.task_id,
|
|
3994
4234
|
status: message.status,
|
|
@@ -4000,7 +4240,9 @@ export class ClaudeAcpAgent {
|
|
|
4000
4240
|
subagents.discardPending(message.tool_use_id);
|
|
4001
4241
|
// The subagent streams no more text under this parent.
|
|
4002
4242
|
streamedBlocksByParent.delete(message.tool_use_id);
|
|
4243
|
+
droppedStreams.delete(message.tool_use_id);
|
|
4003
4244
|
}
|
|
4245
|
+
await endBackgroundTerminal(message.task_id);
|
|
4004
4246
|
settleLiveTask(message.task_id);
|
|
4005
4247
|
break;
|
|
4006
4248
|
case "task_updated":
|
|
@@ -4009,6 +4251,14 @@ export class ClaudeAcpAgent {
|
|
|
4009
4251
|
break;
|
|
4010
4252
|
}
|
|
4011
4253
|
await asyncTasks.taskUpdated(message.task_id, message.patch);
|
|
4254
|
+
// A task moved to the background no longer blocks the tool
|
|
4255
|
+
// call that spawned it, nor its requests the foreground work.
|
|
4256
|
+
if (message.patch.is_backgrounded !== undefined) {
|
|
4257
|
+
const task = session.liveBackgroundTasks.get(message.task_id);
|
|
4258
|
+
if (task)
|
|
4259
|
+
task.backgrounded = message.patch.is_backgrounded;
|
|
4260
|
+
this.syncAwaitingUser(params.sessionId, session);
|
|
4261
|
+
}
|
|
4012
4262
|
// terminal-status task_updated patch and a (deduplicated)
|
|
4013
4263
|
// task_notification when a task settles, but only the patch is
|
|
4014
4264
|
// guaranteed per transition — prune on it too so the registry
|
|
@@ -4017,10 +4267,13 @@ export class ClaudeAcpAgent {
|
|
|
4017
4267
|
if (message.patch.status === "completed" ||
|
|
4018
4268
|
message.patch.status === "failed" ||
|
|
4019
4269
|
message.patch.status === "killed") {
|
|
4020
|
-
await subagents.finishTask(message.task_id, message.patch.status, sendUpdate);
|
|
4270
|
+
await subagents.finishTask(message.task_id, message.patch.status, sendUpdate, undefined, { error: message.patch.status === "failed" ? message.patch.error : undefined });
|
|
4021
4271
|
const parentToolUseId = session.liveBackgroundTasks.get(message.task_id)?.parentToolUseId;
|
|
4022
|
-
if (parentToolUseId)
|
|
4272
|
+
if (parentToolUseId) {
|
|
4023
4273
|
streamedBlocksByParent.delete(parentToolUseId);
|
|
4274
|
+
droppedStreams.delete(parentToolUseId);
|
|
4275
|
+
}
|
|
4276
|
+
await endBackgroundTerminal(message.task_id);
|
|
4024
4277
|
settleLiveTask(message.task_id);
|
|
4025
4278
|
}
|
|
4026
4279
|
else if (message.patch.status === "running" ||
|
|
@@ -4230,6 +4483,7 @@ export class ClaudeAcpAgent {
|
|
|
4230
4483
|
}
|
|
4231
4484
|
break;
|
|
4232
4485
|
case "result": {
|
|
4486
|
+
session.modelRequestSinceResult = false;
|
|
4233
4487
|
// The result ends the model turn. A background task that still
|
|
4234
4488
|
// waits for its tool call id gets its spawn now, without the id.
|
|
4235
4489
|
await asyncTasks.releaseHeld();
|
|
@@ -4264,7 +4518,7 @@ export class ClaudeAcpAgent {
|
|
|
4264
4518
|
// the map in that case).
|
|
4265
4519
|
if (!isAutonomousResult) {
|
|
4266
4520
|
recordResultForOrphanCommands();
|
|
4267
|
-
await ensureActiveTurn(message.user_message_uuid);
|
|
4521
|
+
await ensureActiveTurn(message.user_message_uuid, message.user_message_uuids);
|
|
4268
4522
|
// A result that names a steer consumed it (see
|
|
4269
4523
|
// Turn.steeredEchoes), so the steered work is done: retire
|
|
4270
4524
|
// its echo — the CLI may never have replayed it — and, with
|
|
@@ -4325,13 +4579,10 @@ export class ClaudeAcpAgent {
|
|
|
4325
4579
|
// orphan skip) and the SDK follows it with a trailing
|
|
4326
4580
|
// `session_state_changed: idle` — record the debt so the idle
|
|
4327
4581
|
// handler absorbs that idle rather than reading it as a turn the
|
|
4328
|
-
// SDK abandoned (issue #825).
|
|
4329
|
-
// turn
|
|
4330
|
-
// guard
|
|
4331
|
-
//
|
|
4332
|
-
// the debt there instead) — counting here too would double it.
|
|
4333
|
-
// Results skipped while cancelled with NO active turn — orphaned
|
|
4334
|
-
// queued turns the SDK still ran, or a force-cancelled turn's
|
|
4582
|
+
// SDK abandoned (issue #825). That includes the result of a
|
|
4583
|
+
// cancelled turn, which settles it at the `session.cancelled`
|
|
4584
|
+
// guard below. Results skipped while cancelled with NO active
|
|
4585
|
+
// turn — orphaned queued turns the SDK still ran, or a force-cancelled turn's
|
|
4335
4586
|
// late result after the backstop settled it — get no such settle,
|
|
4336
4587
|
// so their trailers must be counted here or they'd later be read
|
|
4337
4588
|
// as the next healthy turn being abandoned and false-fail it.
|
|
@@ -4344,20 +4595,17 @@ export class ClaudeAcpAgent {
|
|
|
4344
4595
|
// but a held turn settling AT a followup result unblocks the
|
|
4345
4596
|
// client at exactly that point, making the race the common
|
|
4346
4597
|
// case.
|
|
4347
|
-
//
|
|
4348
|
-
// turn's OWN result — a followup result arriving inside the
|
|
4349
|
-
// cancel window still gets its own trailer and must be counted,
|
|
4350
|
-
// or that idle would later false-fail the next prompt.
|
|
4351
|
-
// A second exclusion: a steered turn's own results (see
|
|
4598
|
+
// One exclusion: a steered turn's own results (see
|
|
4352
4599
|
// Turn.steeredEchoes). One idle covers the whole interrupted +
|
|
4353
4600
|
// steered sequence and that idle settles the turn, so counting
|
|
4354
4601
|
// either result would leave a debt that swallows it. Autonomous
|
|
4355
4602
|
// results inside a steered turn keep their own trailers, and so
|
|
4356
4603
|
// does a result that named the steer: it took the turn out of the
|
|
4357
|
-
// steer lane above and settles at the result, not the idle.
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4604
|
+
// steer lane above and settles at the result, not the idle. So
|
|
4605
|
+
// does a cancelled steered turn's result, which settles it at the
|
|
4606
|
+
// `session.cancelled` guard below.
|
|
4607
|
+
const owesTrailingIdle = isAutonomousResult || !isSteering(session.activeTurn) || session.cancelled;
|
|
4608
|
+
if (owesTrailingIdle) {
|
|
4361
4609
|
session.owedTrailingIdles++;
|
|
4362
4610
|
}
|
|
4363
4611
|
// Accumulate usage into the user turn's tally. Skip autonomous
|
|
@@ -4443,6 +4691,26 @@ export class ClaudeAcpAgent {
|
|
|
4443
4691
|
}),
|
|
4444
4692
|
});
|
|
4445
4693
|
}
|
|
4694
|
+
// A model cycle that Claude Code started on its own ends at its
|
|
4695
|
+
// result, after its usage, also after a cancel. A turn that
|
|
4696
|
+
// Claude Code takes in takes the cycle over, so a cycle that
|
|
4697
|
+
// still runs owns this result, whatever its origin says. A
|
|
4698
|
+
// placeholder result (`num_turns: 0`, see the autonomous lane
|
|
4699
|
+
// below) comes before the shared followup runs, so it ends
|
|
4700
|
+
// nothing. The diagnostic of an ExitPlanMode answer that
|
|
4701
|
+
// interrupted the cycle ends it `cancelled`, as it ends a turn,
|
|
4702
|
+
// unless a held turn continues the accepted plan in a fresh
|
|
4703
|
+
// context: that turn takes the cycle over (see the autonomous
|
|
4704
|
+
// lane below).
|
|
4705
|
+
const exitPlanInterruption = pendingExitPlanModeInterruption !== undefined &&
|
|
4706
|
+
isExitPlanInterruptionResult(message, pendingExitPlanModeInterruption);
|
|
4707
|
+
const continuesPlan = exitPlanInterruption &&
|
|
4708
|
+
isHeldOpen(session.activeTurn) &&
|
|
4709
|
+
pendingExitPlanContextReset?.toolUseId ===
|
|
4710
|
+
pendingExitPlanModeInterruption?.toolUseId;
|
|
4711
|
+
if (!continuesPlan && !(isAutonomousResult && message.num_turns === 0)) {
|
|
4712
|
+
this.endAutonomousCycle(params.sessionId, autonomousCycleEnd(message, resultModelUsage, session.cancelled || exitPlanInterruption || isAbortedResult(message)));
|
|
4713
|
+
}
|
|
4446
4714
|
if (session.cancelled) {
|
|
4447
4715
|
session.pendingExitPlanModeInterruption = undefined;
|
|
4448
4716
|
session.pendingExitPlanContextReset = undefined;
|
|
@@ -4450,6 +4718,21 @@ export class ClaudeAcpAgent {
|
|
|
4450
4718
|
await clearFailuresFromEarlierTurns();
|
|
4451
4719
|
stopReason = "cancelled";
|
|
4452
4720
|
}
|
|
4721
|
+
// The cancelled turn ends at the result of the work the
|
|
4722
|
+
// interrupt stopped: its own result, or for a turn held open
|
|
4723
|
+
// for its subagents (see Turn.deferredSettle), its follow-up's
|
|
4724
|
+
// aborted one. A follow-up result that is not aborted, such as
|
|
4725
|
+
// a placeholder or one that ended before the interrupt landed,
|
|
4726
|
+
// belongs to other work. Claude Code ends the stopped work
|
|
4727
|
+
// first, then goes on with its queue, so the idle can come
|
|
4728
|
+
// much later, after a follow-up. That idle, the next echo's
|
|
4729
|
+
// hand-off, and the force-cancel backstop settle a turn whose
|
|
4730
|
+
// result never comes.
|
|
4731
|
+
const active = session.activeTurn;
|
|
4732
|
+
const stoppedWork = !isAutonomousResult || (isHeldOpen(active) && isAbortedResult(message));
|
|
4733
|
+
if (active && !active.settled && stoppedWork) {
|
|
4734
|
+
await settleActive(cancelledOutcome(session, active));
|
|
4735
|
+
}
|
|
4453
4736
|
break;
|
|
4454
4737
|
}
|
|
4455
4738
|
// A held turn (see Turn.deferredSettle) settles at its
|
|
@@ -4486,8 +4769,11 @@ export class ClaudeAcpAgent {
|
|
|
4486
4769
|
pendingExitPlanModeInterruption.toolUseId) {
|
|
4487
4770
|
// The fresh query has none of this one's background
|
|
4488
4771
|
// tasks, so the hold can never drain there: drop it, and
|
|
4489
|
-
// let the continuation's own result settle the turn.
|
|
4772
|
+
// let the continuation's own result settle the turn. The
|
|
4773
|
+
// continuation is the turn's work again, which takes
|
|
4774
|
+
// over the follow-up cycle that planned it.
|
|
4490
4775
|
heldTurn.deferredSettle = undefined;
|
|
4776
|
+
this.startForeground(params.sessionId, heldTurn);
|
|
4491
4777
|
await this.exitPlan.restart(params.sessionId, session, pendingExitPlanContextReset);
|
|
4492
4778
|
return;
|
|
4493
4779
|
}
|
|
@@ -4754,9 +5040,10 @@ export class ClaudeAcpAgent {
|
|
|
4754
5040
|
// subagent never holds the prompt hostage.
|
|
4755
5041
|
//
|
|
4756
5042
|
// is_error/auth already settled via failActive (activeTurn is null
|
|
4757
|
-
// then, so both branches no-op);
|
|
4758
|
-
//
|
|
4759
|
-
//
|
|
5043
|
+
// then, so both branches no-op); a cancelled turn settles at
|
|
5044
|
+
// the `session.cancelled` guard above, or on the idle/abort
|
|
5045
|
+
// path when its result never comes. settleActive is idempotent,
|
|
5046
|
+
// so a duplicate idle is a no-op.
|
|
4760
5047
|
//
|
|
4761
5048
|
// A result also closes this compaction lifecycle — before the
|
|
4762
5049
|
// settle, so a `cancelled` terminal for an entity the runtime
|
|
@@ -4810,43 +5097,14 @@ export class ClaudeAcpAgent {
|
|
|
4810
5097
|
// reset, and the synthetic-auth/system/local-command paths
|
|
4811
5098
|
// `break` early too. Block indices restart at 0 each message, so
|
|
4812
5099
|
// leftover entries would otherwise collide with this message's
|
|
4813
|
-
// blocks and re-emit (or truncate) already-streamed text.
|
|
4814
|
-
//
|
|
4815
|
-
//
|
|
4816
|
-
//
|
|
4817
|
-
//
|
|
5100
|
+
// blocks and re-emit (or truncate) already-streamed text. Keyed by
|
|
5101
|
+
// the stream, so a subagent's message can't clear the top-level
|
|
5102
|
+
// record. Fires once, before any of this message's blocks, so it
|
|
5103
|
+
// doesn't disturb the mid-message turn-activation path the way
|
|
5104
|
+
// resetting on turn activation would. The record's drop latch
|
|
5105
|
+
// (see below) resets with it.
|
|
4818
5106
|
streamedBlocksOf(message.parent_tool_use_id).length = 0;
|
|
4819
|
-
|
|
4820
|
-
// Accumulate the text/thinking actually streamed live, so the
|
|
4821
|
-
// `assistant` case below can diff its assembled blocks against what
|
|
4822
|
-
// already reached the client as chunks and forward only the
|
|
4823
|
-
// remainder. Each stream (top level or one subagent) keeps its own
|
|
4824
|
-
// record, so a subagent cannot attribute content to another stream.
|
|
4825
|
-
// Contiguous deltas of the same block (same index and type) extend
|
|
4826
|
-
// the current entry; anything else opens a new one.
|
|
4827
|
-
if (message.event.type === "content_block_delta") {
|
|
4828
|
-
const delta = message.event.delta;
|
|
4829
|
-
const chunk = delta.type === "text_delta"
|
|
4830
|
-
? { type: "text", text: delta.text }
|
|
4831
|
-
: delta.type === "thinking_delta"
|
|
4832
|
-
? { type: "thinking", text: delta.thinking }
|
|
4833
|
-
: undefined;
|
|
4834
|
-
// Skip empty deltas (some gateways emit empty thinking chunks —
|
|
4835
|
-
// #793): appending "" is a no-op, but pushing a "" entry would
|
|
4836
|
-
// create a block the consolidated handler's `text.length > 0`
|
|
4837
|
-
// guard can never consume, stalling the diff cursor and
|
|
4838
|
-
// re-emitting the next block as a duplicate.
|
|
4839
|
-
if (chunk?.text) {
|
|
4840
|
-
const index = message.event.index;
|
|
4841
|
-
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
4842
|
-
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
4843
|
-
if (last && last.index === index && last.type === chunk.type) {
|
|
4844
|
-
last.text += chunk.text;
|
|
4845
|
-
}
|
|
4846
|
-
else {
|
|
4847
|
-
streamedBlocks.push({ index, type: chunk.type, text: chunk.text });
|
|
4848
|
-
}
|
|
4849
|
-
}
|
|
5107
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
4850
5108
|
}
|
|
4851
5109
|
if (message.parent_tool_use_id === null &&
|
|
4852
5110
|
(message.event.type === "message_start" || message.event.type === "message_delta")) {
|
|
@@ -4900,7 +5158,71 @@ export class ClaudeAcpAgent {
|
|
|
4900
5158
|
});
|
|
4901
5159
|
}
|
|
4902
5160
|
}
|
|
4903
|
-
|
|
5161
|
+
// After a cancel, output streams to the client until the client is
|
|
5162
|
+
// told that the stopped work ended (see workEndReported), so what
|
|
5163
|
+
// the interrupt flushes before then still goes out, as both
|
|
5164
|
+
// protocol versions allow. After that, while the window after the
|
|
5165
|
+
// cancel is open, streamed text, thinking and tool calls are
|
|
5166
|
+
// dropped like the consolidated messages they belong to (see the
|
|
5167
|
+
// `cancelled` check in the user/assistant case). Otherwise the
|
|
5168
|
+
// answer to a prompt the cancel abandoned, which Claude Code had
|
|
5169
|
+
// already taken off its queue, would stream to the client after the
|
|
5170
|
+
// cancel was reported.
|
|
5171
|
+
//
|
|
5172
|
+
// Where the consolidated message delivers the text (the same
|
|
5173
|
+
// condition as in the user/assistant case), a stream that loses a
|
|
5174
|
+
// delta keeps dropping until the record below resets: at the next
|
|
5175
|
+
// message, or when a consolidated message consumes the record. If
|
|
5176
|
+
// the next prompt's echo ends the window mid-block, the record then
|
|
5177
|
+
// stays empty and the consolidated message delivers the block's
|
|
5178
|
+
// whole text once. A record holding only the block's tail is not a
|
|
5179
|
+
// prefix of the text, so the text would be sent again (see
|
|
5180
|
+
// unstreamedRemainder).
|
|
5181
|
+
const streamKey = message.parent_tool_use_id ?? "";
|
|
5182
|
+
if (session.cancelled && this.workEndReported(params.sessionId, session)) {
|
|
5183
|
+
if (message.parent_tool_use_id === null || forwardsSubagentText()) {
|
|
5184
|
+
droppedStreams.add(streamKey);
|
|
5185
|
+
}
|
|
5186
|
+
break;
|
|
5187
|
+
}
|
|
5188
|
+
if (droppedStreams.has(streamKey)) {
|
|
5189
|
+
break;
|
|
5190
|
+
}
|
|
5191
|
+
// Accumulate the text/thinking actually streamed live, so the
|
|
5192
|
+
// `assistant` case below can diff its assembled blocks against what
|
|
5193
|
+
// already reached the client as chunks and forward only the
|
|
5194
|
+
// remainder. Each stream (top level or one subagent) keeps its own
|
|
5195
|
+
// record, so a subagent cannot attribute content to another stream.
|
|
5196
|
+
// Contiguous deltas of the same block (same index and type) extend
|
|
5197
|
+
// the current entry; anything else opens a new one.
|
|
5198
|
+
if (message.event.type === "content_block_delta") {
|
|
5199
|
+
const delta = message.event.delta;
|
|
5200
|
+
const chunk = delta.type === "text_delta"
|
|
5201
|
+
? { type: "text", text: delta.text }
|
|
5202
|
+
: delta.type === "thinking_delta"
|
|
5203
|
+
? { type: "thinking", text: delta.thinking }
|
|
5204
|
+
: undefined;
|
|
5205
|
+
// Skip empty deltas (some gateways emit empty thinking chunks —
|
|
5206
|
+
// #793): appending "" is a no-op, but pushing a "" entry would
|
|
5207
|
+
// create a block the consolidated handler's `text.length > 0`
|
|
5208
|
+
// guard can never consume, stalling the diff cursor and
|
|
5209
|
+
// re-emitting the next block as a duplicate.
|
|
5210
|
+
if (chunk?.text) {
|
|
5211
|
+
const index = message.event.index;
|
|
5212
|
+
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
5213
|
+
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
5214
|
+
if (last && last.index === index && last.type === chunk.type) {
|
|
5215
|
+
last.text += chunk.text;
|
|
5216
|
+
}
|
|
5217
|
+
else {
|
|
5218
|
+
streamedBlocks.push({ index, type: chunk.type, text: chunk.text });
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
// The stream sees a tool_use first, so the tool's PostToolUse hook
|
|
5223
|
+
// is registered here, and publishes through this client: a
|
|
5224
|
+
// subagent's hook update must reach the child session too.
|
|
5225
|
+
for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
4904
5226
|
clientCapabilities: this.clientCapabilities,
|
|
4905
5227
|
toolCallCapabilities: this.toolCallCapabilities,
|
|
4906
5228
|
cwd: session.cwd,
|
|
@@ -4955,7 +5277,7 @@ export class ClaudeAcpAgent {
|
|
|
4955
5277
|
if (session.activeTurn !== queued) {
|
|
4956
5278
|
if (session.activeTurn) {
|
|
4957
5279
|
// Hand off the previous turn. If a cancel is pending for it
|
|
4958
|
-
// (its
|
|
5280
|
+
// (neither its result nor its idle has arrived), settle it
|
|
4959
5281
|
// "cancelled" per the ACP contract rather than "end_turn" —
|
|
4960
5282
|
// otherwise a cancel followed quickly by the next prompt
|
|
4961
5283
|
// would report the cancelled turn as a normal completion.
|
|
@@ -5033,6 +5355,13 @@ export class ClaudeAcpAgent {
|
|
|
5033
5355
|
}
|
|
5034
5356
|
}
|
|
5035
5357
|
if (session.cancelled) {
|
|
5358
|
+
// The work the cancel stopped ends its tools with error results.
|
|
5359
|
+
// Until the client is told that the work ended, they close the
|
|
5360
|
+
// tool calls it opened. Everything else that work sends is
|
|
5361
|
+
// dropped, and so is everything after that report.
|
|
5362
|
+
if (message.type === "user" && !this.workEndReported(params.sessionId, session)) {
|
|
5363
|
+
await sendInterruptedToolResults(message);
|
|
5364
|
+
}
|
|
5036
5365
|
break;
|
|
5037
5366
|
}
|
|
5038
5367
|
// Synthetic assistant frames carry the CLI's local-command output.
|
|
@@ -5164,6 +5493,7 @@ export class ClaudeAcpAgent {
|
|
|
5164
5493
|
const streamed = streamedBlocksOf(message.parent_tool_use_id);
|
|
5165
5494
|
content = unstreamedRemainder(message.message.content, streamed);
|
|
5166
5495
|
streamed.length = 0;
|
|
5496
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
5167
5497
|
}
|
|
5168
5498
|
else if (message.type === "assistant") {
|
|
5169
5499
|
// Nested text/thinking stays internal for a client that does
|
|
@@ -5176,19 +5506,41 @@ export class ClaudeAcpAgent {
|
|
|
5176
5506
|
}
|
|
5177
5507
|
const acceptedPlanToolUseId = observeExitPlanToolResults(message, content, session);
|
|
5178
5508
|
let backgroundedToolCalls = new Set();
|
|
5509
|
+
let runningTerminal;
|
|
5179
5510
|
if (message.type === "user") {
|
|
5180
5511
|
rememberResolvedToolNames(session, content);
|
|
5181
5512
|
const backgroundBashTask = backgroundBashTaskFromToolResult(content, message.tool_use_result, session.toolUseCache);
|
|
5182
5513
|
if (backgroundBashTask)
|
|
5183
5514
|
await asyncTasks.taskBackgrounded(backgroundBashTask);
|
|
5515
|
+
// The command's terminal runs until its task ends, unless the
|
|
5516
|
+
// task has ended already (a fast command can end before the tool
|
|
5517
|
+
// result comes).
|
|
5518
|
+
const terminalId = backgroundBashTask?.toolCallId;
|
|
5519
|
+
if (terminalId &&
|
|
5520
|
+
this.toolCallCapabilities.terminalOutput &&
|
|
5521
|
+
!this.toolCallCapabilities.air.client &&
|
|
5522
|
+
session.liveBackgroundTasks.has(backgroundBashTask.taskId)) {
|
|
5523
|
+
runningTerminal = terminalId;
|
|
5524
|
+
(session.backgroundTerminals ??= new Map()).set(backgroundBashTask.taskId, {
|
|
5525
|
+
toolCallId: terminalId,
|
|
5526
|
+
...(message.parent_tool_use_id
|
|
5527
|
+
? { parentToolUseId: message.parent_tool_use_id }
|
|
5528
|
+
: {}),
|
|
5529
|
+
});
|
|
5530
|
+
}
|
|
5184
5531
|
await asyncTasks.toolResults(content);
|
|
5185
5532
|
backgroundedToolCalls = backgroundedBashToolCallIds(content, session.toolUseCache, asyncTasks, backgroundBashTask);
|
|
5186
5533
|
const resumedAgentId = resumedNativeSubagentId(message.tool_use_result);
|
|
5187
5534
|
if (resumedAgentId) {
|
|
5188
|
-
|
|
5189
|
-
await subagents.taskResumed(resumedAgentId, sendUpdate, sendMessageResumePrompt(session.toolUseCache, resumedAgentId, Array.isArray(content)
|
|
5535
|
+
const resume = sendMessageResume(session.toolUseCache, resumedAgentId, Array.isArray(content)
|
|
5190
5536
|
? content.flatMap((block) => block.type === "tool_result" ? [block.tool_use_id] : [])
|
|
5191
|
-
: [])
|
|
5537
|
+
: []);
|
|
5538
|
+
// In the RFD form the result can arrive after the run it
|
|
5539
|
+
// started ended, and then resumes nothing.
|
|
5540
|
+
if (!subagents.isEndedDelegation(resumedAgentId, resume?.toolUseId)) {
|
|
5541
|
+
resumeLiveTask(resumedAgentId);
|
|
5542
|
+
await subagents.sendMessageResumed(resumedAgentId, sendUpdate, resume?.text, resume?.toolUseId);
|
|
5543
|
+
}
|
|
5192
5544
|
}
|
|
5193
5545
|
}
|
|
5194
5546
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
@@ -5211,7 +5563,7 @@ export class ClaudeAcpAgent {
|
|
|
5211
5563
|
// filtered out of `content` above; blocks that do pass through
|
|
5212
5564
|
// (e.g. a subagent image) carry the stamped parentToolUseId
|
|
5213
5565
|
// meta and are excluded there.
|
|
5214
|
-
await sendUpdate(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled));
|
|
5566
|
+
await sendUpdate(withRunningTerminal(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled), runningTerminal));
|
|
5215
5567
|
}
|
|
5216
5568
|
break;
|
|
5217
5569
|
}
|
|
@@ -5235,10 +5587,14 @@ export class ClaudeAcpAgent {
|
|
|
5235
5587
|
if (toolCallId === null || !session.emittedToolCalls.has(toolCallId)) {
|
|
5236
5588
|
break;
|
|
5237
5589
|
}
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5590
|
+
// The beat is in the subagent whose Agent call is
|
|
5591
|
+
// `parent_tool_use_id`, unless it reports on that Agent call
|
|
5592
|
+
// itself: the Agent call's own heartbeat falls back to its id, and
|
|
5593
|
+
// a tool call is never its own parent.
|
|
5594
|
+
const subagentParentToolUseId = message.parent_tool_use_id &&
|
|
5595
|
+
message.parent_tool_use_id !== toolCallId &&
|
|
5596
|
+
[...session.liveBackgroundTasks.values()].some((task) => task.isSubagent && task.parentToolUseId === message.parent_tool_use_id)
|
|
5597
|
+
? message.parent_tool_use_id
|
|
5242
5598
|
: undefined;
|
|
5243
5599
|
// `tool_name` names the tool that reports the beat. When the beat
|
|
5244
5600
|
// falls back to the parent call, AIR gets the name of that call,
|
|
@@ -5433,6 +5789,16 @@ export class ClaudeAcpAgent {
|
|
|
5433
5789
|
return;
|
|
5434
5790
|
}
|
|
5435
5791
|
session.cancelled = true;
|
|
5792
|
+
// Before anything awaits, so a model request that the consumer reads in
|
|
5793
|
+
// the meantime is already judged against these prompts.
|
|
5794
|
+
session.cancelledPrompts ??= new Set();
|
|
5795
|
+
for (const turn of session.turnQueue ?? []) {
|
|
5796
|
+
if (turn.settled)
|
|
5797
|
+
continue;
|
|
5798
|
+
session.cancelledPrompts.add(turn.promptUuid);
|
|
5799
|
+
for (const uuid of turn.steeredUuids ?? [])
|
|
5800
|
+
session.cancelledPrompts.add(uuid);
|
|
5801
|
+
}
|
|
5436
5802
|
for (const turn of session.turnQueue ?? [])
|
|
5437
5803
|
turn.localCommandAbort?.abort();
|
|
5438
5804
|
session.pendingExitPlanModeInterruption = undefined;
|
|
@@ -5451,17 +5817,29 @@ export class ClaudeAcpAgent {
|
|
|
5451
5817
|
// their compaction is running. Close it before ANY cancelled prompt
|
|
5452
5818
|
// resolves, not just the active-turn idle/backstop paths.
|
|
5453
5819
|
await session.contextCompaction?.interrupt();
|
|
5820
|
+
// A cancel drops the state of each tool call (the session that a
|
|
5821
|
+
// permission request emitted it in, the fields the client holds, the hook
|
|
5822
|
+
// callbacks), even when publishing fails. AIR's draft ends every child
|
|
5823
|
+
// here, so a later update of an ended child's tool call is then dropped
|
|
5824
|
+
// instead of reaching that child's session. The RFD form keeps the state:
|
|
5825
|
+
// a background subagent can keep running after a cancel
|
|
5826
|
+
// (agentclientprotocol/agent-client-protocol#2308), and its tool call that
|
|
5827
|
+
// a permission request emitted on the root before the child was announced
|
|
5828
|
+
// must stay on the root, which the RFD requires. Each tool call drops its
|
|
5829
|
+
// own state when it ends.
|
|
5454
5830
|
try {
|
|
5455
|
-
await session.nativeSubagentRuntime?.
|
|
5831
|
+
await session.nativeSubagentRuntime?.parentCancelled(session.nativeSubagentDeliver ??
|
|
5456
5832
|
(async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification))));
|
|
5457
5833
|
}
|
|
5458
5834
|
catch (error) {
|
|
5459
5835
|
this.logger.error(`Session ${params.sessionId}: failed to publish cancelled subagent state`, error);
|
|
5460
5836
|
}
|
|
5461
5837
|
finally {
|
|
5462
|
-
session.
|
|
5463
|
-
|
|
5464
|
-
|
|
5838
|
+
if (session.nativeSubagentRuntime?.form !== "rfd") {
|
|
5839
|
+
session.eagerToolCallSessions?.clear();
|
|
5840
|
+
session.toolCallFields?.clear();
|
|
5841
|
+
clearHookCallbacks(params.sessionId);
|
|
5842
|
+
}
|
|
5465
5843
|
}
|
|
5466
5844
|
// The user messages this cancel abandons while Claude Code may still have
|
|
5467
5845
|
// them queued: they are dropped from its queue before the interrupt.
|
|
@@ -5483,9 +5861,10 @@ export class ClaudeAcpAgent {
|
|
|
5483
5861
|
// never decrements the count) to miss.
|
|
5484
5862
|
const lifecycleLane = session.msgLifecycleV1 === true;
|
|
5485
5863
|
// Settle queued turns that haven't started yet (no echo seen) right away —
|
|
5486
|
-
// they have no in-flight SDK work to interrupt. The
|
|
5487
|
-
//
|
|
5488
|
-
// backstop below). Mirrors the old pendingMessages
|
|
5864
|
+
// they have no in-flight SDK work to interrupt. The consumer settles the
|
|
5865
|
+
// active turn at its interrupted result, or at the idle when that never
|
|
5866
|
+
// comes (or via the backstop below). Mirrors the old pendingMessages
|
|
5867
|
+
// cancellation.
|
|
5489
5868
|
const orphanedTurns = [];
|
|
5490
5869
|
if (session.turnQueue) {
|
|
5491
5870
|
for (const turn of session.turnQueue) {
|
|
@@ -5571,14 +5950,19 @@ export class ClaudeAcpAgent {
|
|
|
5571
5950
|
// whose session sits idle gets no trailing idle (the interrupt emits
|
|
5572
5951
|
// nothing; with no state events consumed yet, the same is assumed).
|
|
5573
5952
|
// A held turn whose followup cycle is live (running, or blocked on a
|
|
5574
|
-
// permission request) is left to that
|
|
5575
|
-
//
|
|
5576
|
-
//
|
|
5577
|
-
//
|
|
5578
|
-
//
|
|
5953
|
+
// permission request) is left to that cycle's interrupted result, like any
|
|
5954
|
+
// live turn, or to the idle when no result comes. The captured outcome
|
|
5955
|
+
// preserves usage and metadata (issue #844), while reporter state makes a
|
|
5956
|
+
// late checkpoint response harmless.
|
|
5957
|
+
// Since CLI 2.1.270, Claude Code reports `running` for as long as
|
|
5958
|
+
// background agents live, so the session state cannot tell a live
|
|
5959
|
+
// followup from a hold. With request markers, a followup is live when one
|
|
5960
|
+
// of its model requests started after the turn's result.
|
|
5579
5961
|
{
|
|
5580
5962
|
const active = session.activeTurn;
|
|
5581
|
-
const followupLive = session.
|
|
5963
|
+
const followupLive = session.requestMarkerListsV1
|
|
5964
|
+
? session.modelRequestSinceResult === true
|
|
5965
|
+
: session.lastSessionState === "running" || session.lastSessionState === "requires_action";
|
|
5582
5966
|
if (active &&
|
|
5583
5967
|
(active.settling || (isHeldOpen(active) && !followupLive)) &&
|
|
5584
5968
|
(active.deferredSettle ?? active.settlingOutcome)) {
|
|
@@ -5618,7 +6002,7 @@ export class ClaudeAcpAgent {
|
|
|
5618
6002
|
}
|
|
5619
6003
|
}
|
|
5620
6004
|
// A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
|
|
5621
|
-
// consumer at
|
|
6005
|
+
// consumer at its interrupted result, or at the idle, like any live turn. But the
|
|
5622
6006
|
// steer lane pays for its last result's trailer by settling on it, which
|
|
5623
6007
|
// this cancel pre-empts, so count that trailer here or it arrives un-owed
|
|
5624
6008
|
// against the next prompt and false-fails it (issue #825). Over-counting
|
|
@@ -5649,6 +6033,10 @@ export class ClaudeAcpAgent {
|
|
|
5649
6033
|
}
|
|
5650
6034
|
// Before the interrupt: once it stops the running turn, Claude Code starts
|
|
5651
6035
|
// the next queued message right away.
|
|
6036
|
+
// A prompt or steer that arrived while this cancel awaited is abandoned
|
|
6037
|
+
// too, after the record above was taken.
|
|
6038
|
+
for (const uuid of abandoned)
|
|
6039
|
+
(session.cancelledPrompts ??= new Set()).add(uuid);
|
|
5652
6040
|
this.dropQueuedMessages(params.sessionId, session.query, abandoned);
|
|
5653
6041
|
const interrupt = session.query.interrupt();
|
|
5654
6042
|
if (!options.awaitInterrupt) {
|
|
@@ -5769,6 +6157,9 @@ export class ClaudeAcpAgent {
|
|
|
5769
6157
|
catch (error) {
|
|
5770
6158
|
this.logger.error(`Session ${sessionId}: cancellation failed during teardown`, error);
|
|
5771
6159
|
}
|
|
6160
|
+
// The turns end before this returns; so does a model cycle that Claude
|
|
6161
|
+
// Code started on its own, which no turn waits for.
|
|
6162
|
+
this.endAutonomousCycle(sessionId, { outcome: { stopReason: "cancelled" } });
|
|
5772
6163
|
// cancel() arms the force-cancel floor and interrupts gracefully, but a
|
|
5773
6164
|
// wedged consumer only wakes when `cancelController` aborts — closeQueryStream
|
|
5774
6165
|
// below doesn't touch it. Since we're tearing the session down anyway, wake
|
|
@@ -5910,7 +6301,12 @@ export class ClaudeAcpAgent {
|
|
|
5910
6301
|
// A replay rebuilds the client view, so its plan goes out again.
|
|
5911
6302
|
if (session?.taskState)
|
|
5912
6303
|
forgetPublishedTaskPlan(session.taskState);
|
|
5913
|
-
|
|
6304
|
+
// A subagent's text replays onto the root only for a client that gets it
|
|
6305
|
+
// there live: one without native subagent sessions that asked for the
|
|
6306
|
+
// flattened transcript. With native sessions the text goes to the child
|
|
6307
|
+
// session, which AIR's replay restores and the RFD form leaves out for now.
|
|
6308
|
+
const forwardSubagentText = !clientSupportsSubagents(this.clientCapabilities) &&
|
|
6309
|
+
(session?.forwardSubagentText ?? supportsSubagentTranscript(this.clientCapabilities));
|
|
5914
6310
|
const supportsTypedFailures = supportsAirSessionFailures(this.clientCapabilities);
|
|
5915
6311
|
const activeUsageLimit = supportsTypedFailures ? activeUsageLimitMessage(messages) : undefined;
|
|
5916
6312
|
const sessionFailures = session && supportsTypedFailures
|
|
@@ -5929,7 +6325,11 @@ export class ClaudeAcpAgent {
|
|
|
5929
6325
|
})
|
|
5930
6326
|
: undefined;
|
|
5931
6327
|
let replayTurnId;
|
|
5932
|
-
|
|
6328
|
+
// AIR's draft replays each child under a replay id. A client on the RFD
|
|
6329
|
+
// form gets each child under the session id it had live (see
|
|
6330
|
+
// `RfdSubagentReplay`).
|
|
6331
|
+
const form = subagentForm(this.clientCapabilities);
|
|
6332
|
+
const nativeReplayEnabled = form === "air";
|
|
5933
6333
|
const replayCompactionUpdates = clientSupportsCompactionUpdates(this.clientCapabilities);
|
|
5934
6334
|
const replayTerminalStates = new Map();
|
|
5935
6335
|
const replayChildren = new Map();
|
|
@@ -6032,8 +6432,17 @@ export class ClaudeAcpAgent {
|
|
|
6032
6432
|
// Only a history with an Agent or Task launch has subagent transcripts to read.
|
|
6033
6433
|
const subagents = nativeReplayEnabled && replayChildren.size > 0
|
|
6034
6434
|
? await subagentHistory(sessionId)
|
|
6035
|
-
: {
|
|
6435
|
+
: { agents: new Map() };
|
|
6036
6436
|
const replayedSubagents = new Set();
|
|
6437
|
+
const rfdHistory = form === "rfd" &&
|
|
6438
|
+
messages.some((message) => subagentLaunchIds(message.message?.content)
|
|
6439
|
+
.length > 0)
|
|
6440
|
+
? await subagentHistory(sessionId)
|
|
6441
|
+
: { agents: new Map() };
|
|
6442
|
+
const rfdReplay = form === "rfd"
|
|
6443
|
+
? new RfdSubagentReplay(rfdHistory, (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)))
|
|
6444
|
+
: undefined;
|
|
6445
|
+
rfdReplay?.scan(messages, sessionId);
|
|
6037
6446
|
// History persists no task_started frame, so a background task gets its
|
|
6038
6447
|
// spawn from the notification of its stop, with the tool call that
|
|
6039
6448
|
// started it. A task still running at the end of the history gets no
|
|
@@ -6052,6 +6461,8 @@ export class ClaudeAcpAgent {
|
|
|
6052
6461
|
});
|
|
6053
6462
|
// The replay counterpart of the live `task_notification` frame.
|
|
6054
6463
|
const restoreTaskNotification = async (notification) => {
|
|
6464
|
+
if (rfdReplay?.taskNotification(notification.task_id, notification.status))
|
|
6465
|
+
return;
|
|
6055
6466
|
const toolUseId = notification.tool_use_id;
|
|
6056
6467
|
if (!toolUseId)
|
|
6057
6468
|
return;
|
|
@@ -6078,7 +6489,8 @@ export class ClaudeAcpAgent {
|
|
|
6078
6489
|
});
|
|
6079
6490
|
await replayAsyncTasks.taskNotification(notification);
|
|
6080
6491
|
};
|
|
6081
|
-
|
|
6492
|
+
/** Replays `message`, from the transcript of `child` when it is set (the RFD form). */
|
|
6493
|
+
const replayMessage = async (message, child) => {
|
|
6082
6494
|
// A long history is sent in order, but with event-loop turns in between,
|
|
6083
6495
|
// so other sessions and incoming requests are not starved meanwhile.
|
|
6084
6496
|
const pause = messageLoopYielder.maybeYield();
|
|
@@ -6087,6 +6499,10 @@ export class ClaudeAcpAgent {
|
|
|
6087
6499
|
if (pending?.stopped || isReplayHiddenMetaMessage(message)) {
|
|
6088
6500
|
return;
|
|
6089
6501
|
}
|
|
6502
|
+
// In the RFD form a child's history comes from its own transcript, so a
|
|
6503
|
+
// subagent message that the session transcript stores inline is skipped.
|
|
6504
|
+
if (rfdReplay && !child && parentToolUseIdOf(message))
|
|
6505
|
+
return;
|
|
6090
6506
|
if (message.type === "user" &&
|
|
6091
6507
|
message.parent_tool_use_id === null &&
|
|
6092
6508
|
typeof message.uuid === "string" &&
|
|
@@ -6126,11 +6542,14 @@ export class ClaudeAcpAgent {
|
|
|
6126
6542
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
6127
6543
|
let content = message.message.content;
|
|
6128
6544
|
const parentToolUseId = parentToolUseIdOf(message);
|
|
6129
|
-
const replayTargetSessionId =
|
|
6130
|
-
?
|
|
6131
|
-
:
|
|
6545
|
+
const replayTargetSessionId = child
|
|
6546
|
+
? child.sessionId
|
|
6547
|
+
: nativeReplayEnabled && parentToolUseId
|
|
6548
|
+
? await announceReplayChild(parentToolUseId)
|
|
6549
|
+
: sessionId;
|
|
6132
6550
|
if (message.type === "assistant" &&
|
|
6133
6551
|
parentToolUseId &&
|
|
6552
|
+
!child &&
|
|
6134
6553
|
!nativeReplayEnabled &&
|
|
6135
6554
|
!forwardSubagentText) {
|
|
6136
6555
|
content = stripSubagentTextAndThinking(content);
|
|
@@ -6144,6 +6563,9 @@ export class ClaudeAcpAgent {
|
|
|
6144
6563
|
}
|
|
6145
6564
|
// Live, the prompt loop skips this record and the SDK frame reports the stop.
|
|
6146
6565
|
content = isTaskNotificationRecord(message) ? null : stripLocalCommandMetadata(content);
|
|
6566
|
+
if (content !== null && child && rfdReplay) {
|
|
6567
|
+
content = await rfdReplay.childContent(child, message, content);
|
|
6568
|
+
}
|
|
6147
6569
|
if (content === null)
|
|
6148
6570
|
return;
|
|
6149
6571
|
}
|
|
@@ -6215,32 +6637,37 @@ export class ClaudeAcpAgent {
|
|
|
6215
6637
|
if (nativeReplayEnabled && message.type === "assistant") {
|
|
6216
6638
|
for (const toolUseId of subagentLaunchIds(content)) {
|
|
6217
6639
|
await announceReplayChild(toolUseId);
|
|
6218
|
-
const agentId = subagents.
|
|
6640
|
+
const agentId = subagents.agents.get(toolUseId)?.agentId;
|
|
6219
6641
|
if (!agentId || replayedSubagents.has(agentId))
|
|
6220
6642
|
continue;
|
|
6221
6643
|
replayedSubagents.add(agentId);
|
|
6222
|
-
|
|
6223
|
-
try {
|
|
6224
|
-
// With the project directory, the SDK skips its search of every project.
|
|
6225
|
-
// A session opened from another directory falls back to that search.
|
|
6226
|
-
if (subagents.dir) {
|
|
6227
|
-
childMessages = await getSubagentMessages(sessionId, agentId, { dir: subagents.dir });
|
|
6228
|
-
}
|
|
6229
|
-
if (childMessages.length === 0) {
|
|
6230
|
-
childMessages = await getSubagentMessages(sessionId, agentId);
|
|
6231
|
-
}
|
|
6232
|
-
}
|
|
6233
|
-
catch (error) {
|
|
6234
|
-
this.logger.error(`Failed to read the history of subagent ${agentId}:`, error);
|
|
6235
|
-
}
|
|
6644
|
+
const childMessages = await readSubagentMessages(sessionId, agentId, subagents, this.logger);
|
|
6236
6645
|
registerLaunches(childMessages);
|
|
6237
6646
|
for (const childMessage of childMessages)
|
|
6238
6647
|
await replayMessage(childMessage);
|
|
6239
6648
|
}
|
|
6240
6649
|
}
|
|
6650
|
+
// The RFD form announces a child right after its launch, then sends its
|
|
6651
|
+
// own transcript to it.
|
|
6652
|
+
if (rfdReplay && message.type === "assistant") {
|
|
6653
|
+
for (const toolUseId of subagentLaunchIds(content)) {
|
|
6654
|
+
const launched = rfdReplay.childOfLaunch(toolUseId);
|
|
6655
|
+
if (!launched || launched.announced)
|
|
6656
|
+
continue;
|
|
6657
|
+
await rfdReplay.announce(launched);
|
|
6658
|
+
const childMessages = await readSubagentMessages(sessionId, launched.sessionId, rfdHistory, this.logger);
|
|
6659
|
+
rfdReplay.scan(childMessages, launched.sessionId);
|
|
6660
|
+
for (const childMessage of childMessages)
|
|
6661
|
+
await replayMessage(childMessage, launched);
|
|
6662
|
+
await rfdReplay.childReplayed(launched, childMessages);
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6241
6665
|
};
|
|
6242
6666
|
for (const message of messages)
|
|
6243
6667
|
await replayMessage(message);
|
|
6668
|
+
if (rfdReplay && !pending?.stopped) {
|
|
6669
|
+
await rfdReplay.finish(clientSupportsNotices(this.clientCapabilities), sessionId);
|
|
6670
|
+
}
|
|
6244
6671
|
if (nativeReplayEnabled && !pending?.stopped) {
|
|
6245
6672
|
// Claude history persists sidechain messages and Agent/Task tool uses,
|
|
6246
6673
|
// but not task_started/task_updated lifecycle frames. Recover terminal
|
|
@@ -6275,43 +6702,121 @@ export class ClaudeAcpAgent {
|
|
|
6275
6702
|
const response = await this.client.writeTextFile(params);
|
|
6276
6703
|
return response;
|
|
6277
6704
|
}
|
|
6278
|
-
/**
|
|
6279
|
-
*
|
|
6280
|
-
*
|
|
6281
|
-
*
|
|
6282
|
-
|
|
6283
|
-
|
|
6705
|
+
/** Runs `request`, a request that `requestSessionId` sends for the session
|
|
6706
|
+
* `ownerSessionId`. When that is a subagent's child session, the child
|
|
6707
|
+
* reports that it awaits the user while the request is open (the RFD form,
|
|
6708
|
+
* see `NativeSubagentRuntime.awaitingUser`). */
|
|
6709
|
+
withChildAwaitingUser(ownerSessionId, requestSessionId, request) {
|
|
6710
|
+
const runtime = this.sessions[ownerSessionId]?.nativeSubagentRuntime;
|
|
6711
|
+
return runtime && requestSessionId !== ownerSessionId
|
|
6712
|
+
? runtime.awaitingUser(requestSessionId, request)
|
|
6713
|
+
: request();
|
|
6714
|
+
}
|
|
6715
|
+
/** Mark a client request of the subagent `agentID` (absent for the main
|
|
6716
|
+
* conversation) as awaiting user input for exactly the lifetime of its
|
|
6717
|
+
* promise (see `Session.openUserInputs`). Steering consults the open
|
|
6718
|
+
* requests synchronously, so a message arriving while any permission or
|
|
6719
|
+
* elicitation card is open uses non-interrupting SDK delivery. */
|
|
6720
|
+
async withPendingUserInput(sessionId, request, agentID) {
|
|
6284
6721
|
const session = this.sessions[sessionId];
|
|
6285
6722
|
if (!session)
|
|
6286
6723
|
return request();
|
|
6287
|
-
|
|
6288
|
-
|
|
6724
|
+
const input = agentID !== undefined ? { agentID } : {};
|
|
6725
|
+
(session.openUserInputs ??= new Set()).add(input);
|
|
6726
|
+
this.syncAwaitingUser(sessionId, session);
|
|
6289
6727
|
try {
|
|
6290
6728
|
return await request();
|
|
6291
6729
|
}
|
|
6292
6730
|
finally {
|
|
6293
|
-
session.
|
|
6294
|
-
this.syncAwaitingUser(session);
|
|
6295
|
-
}
|
|
6296
|
-
}
|
|
6297
|
-
/** Report whether the session's
|
|
6298
|
-
* a permission request or question
|
|
6299
|
-
* not say which prompt it is for,
|
|
6300
|
-
*
|
|
6301
|
-
*
|
|
6302
|
-
* when a turn activates
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6731
|
+
session.openUserInputs.delete(input);
|
|
6732
|
+
this.syncAwaitingUser(sessionId, session);
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
/** Report whether the session's foreground work awaits the user: exactly
|
|
6736
|
+
* while a permission request or question that blocks it is open (see
|
|
6737
|
+
* {@link blocksForeground}). A request does not say which prompt it is for,
|
|
6738
|
+
* and Claude Code can ask for a queued prompt before the consumer activates
|
|
6739
|
+
* it, so a request is not tied to the work that was running when it
|
|
6740
|
+
* opened. Called when a request opens or closes, when a turn activates, and
|
|
6741
|
+
* when a task registers or moves to the background. */
|
|
6742
|
+
syncAwaitingUser(sessionId, session) {
|
|
6743
|
+
const work = this.foregroundWork.get(sessionId);
|
|
6744
|
+
if (!work)
|
|
6306
6745
|
return;
|
|
6307
|
-
const waiting = (session.
|
|
6308
|
-
if (waiting ===
|
|
6746
|
+
const waiting = [...(session.openUserInputs ?? [])].some((input) => blocksForeground(session, input.agentID));
|
|
6747
|
+
if (waiting === work.awaitingUser)
|
|
6309
6748
|
return;
|
|
6310
|
-
|
|
6749
|
+
work.awaitingUser = waiting;
|
|
6311
6750
|
if (waiting)
|
|
6312
|
-
|
|
6751
|
+
work.events.awaitingUser();
|
|
6752
|
+
else
|
|
6753
|
+
work.events.running();
|
|
6754
|
+
}
|
|
6755
|
+
/** Make `owner` the foreground work of the session `sessionId` (see
|
|
6756
|
+
* {@link ForegroundEvents}): it reports `running`, then waits at once if a
|
|
6757
|
+
* request is open. If work already runs, it goes on under `owner` and
|
|
6758
|
+
* reports nothing new: a turn takes over a cycle that Claude Code started on
|
|
6759
|
+
* its own, and only the owner's end ends the work. */
|
|
6760
|
+
startForeground(sessionId, owner) {
|
|
6761
|
+
if (!this.foregroundEvents)
|
|
6762
|
+
return;
|
|
6763
|
+
const current = this.foregroundWork.get(sessionId);
|
|
6764
|
+
if (current) {
|
|
6765
|
+
current.owner = owner;
|
|
6766
|
+
return;
|
|
6767
|
+
}
|
|
6768
|
+
const events = this.foregroundEvents(sessionId);
|
|
6769
|
+
this.foregroundWork.set(sessionId, { owner, events, awaitingUser: false });
|
|
6770
|
+
events.running();
|
|
6771
|
+
const session = this.sessions[sessionId];
|
|
6772
|
+
if (session)
|
|
6773
|
+
this.syncAwaitingUser(sessionId, session);
|
|
6774
|
+
}
|
|
6775
|
+
/** End the foreground work of the session `sessionId` with `outcome`, or as
|
|
6776
|
+
* failed with `error`, if `owner` is that work. */
|
|
6777
|
+
endForeground(sessionId, owner, end) {
|
|
6778
|
+
const work = this.foregroundWork.get(sessionId);
|
|
6779
|
+
if (work?.owner !== owner)
|
|
6780
|
+
return;
|
|
6781
|
+
this.foregroundWork.delete(sessionId);
|
|
6782
|
+
if ("outcome" in end)
|
|
6783
|
+
work.events.ended(end.outcome);
|
|
6313
6784
|
else
|
|
6314
|
-
|
|
6785
|
+
work.events.failed(end.error);
|
|
6786
|
+
}
|
|
6787
|
+
/** Start a model cycle that Claude Code started on its own as the foreground
|
|
6788
|
+
* work (see {@link ForegroundEvents}) when `message` is its first output:
|
|
6789
|
+
* model output of the main conversation (not a subagent's) while no work
|
|
6790
|
+
* runs. That includes a turn that v1 holds for its background subagents,
|
|
6791
|
+
* whose work ended at its result, so its follow-up is a cycle. Output of
|
|
6792
|
+
* the work a cancel stopped starts nothing (see Session.cancelled). */
|
|
6793
|
+
startAutonomousCycle(sessionId, session, message) {
|
|
6794
|
+
if (!this.foregroundEvents || this.foregroundWork.has(sessionId) || session.cancelled)
|
|
6795
|
+
return;
|
|
6796
|
+
if (message.type !== "assistant" && message.type !== "stream_event")
|
|
6797
|
+
return;
|
|
6798
|
+
if (message.parent_tool_use_id !== null)
|
|
6799
|
+
return;
|
|
6800
|
+
// A turn that Claude Code started is the foreground work already. Without
|
|
6801
|
+
// lifecycle frames, a queued turn's output can come before its echo, so
|
|
6802
|
+
// output while a turn is queued may be that turn's. With them, a turn that
|
|
6803
|
+
// Claude Code has not started has no output yet.
|
|
6804
|
+
const queued = (session.turnQueue ?? []).some((turn) => !turn.settled && turn !== session.activeTurn);
|
|
6805
|
+
if (queued && !session.msgLifecycleV1)
|
|
6806
|
+
return;
|
|
6807
|
+
this.startForeground(sessionId, AUTONOMOUS_CYCLE);
|
|
6808
|
+
}
|
|
6809
|
+
/** Whether the client has been told that the session's work ended: on v2 by
|
|
6810
|
+
* an idle state, so no foreground work is open, including a cycle that
|
|
6811
|
+
* Claude Code started on its own; on v1 by the response to the turn's
|
|
6812
|
+
* prompt, so no turn is active. */
|
|
6813
|
+
workEndReported(sessionId, session) {
|
|
6814
|
+
return this.foregroundEvents ? !this.foregroundWork.has(sessionId) : !session.activeTurn;
|
|
6815
|
+
}
|
|
6816
|
+
/** End the session's model cycle that Claude Code started on its own, if one
|
|
6817
|
+
* runs. */
|
|
6818
|
+
endAutonomousCycle(sessionId, end) {
|
|
6819
|
+
this.endForeground(sessionId, AUTONOMOUS_CYCLE, end);
|
|
6315
6820
|
}
|
|
6316
6821
|
/** Forward a permission request to the client, wiring the tool call's
|
|
6317
6822
|
* `signal` through as a `cancellationSignal`. When the turn is cancelled
|
|
@@ -6319,7 +6824,7 @@ export class ClaudeAcpAgent {
|
|
|
6319
6824
|
* `$/cancel_request`, and our local abort race settles even if the client
|
|
6320
6825
|
* ignores it. A `cancelled` outcome, request rejection, and local abort all
|
|
6321
6826
|
* surface the same "Tool use aborted" the callers already expect. */
|
|
6322
|
-
async requestPermissionFromClient(params, toolName, signal, parentToolUseId, ownerSessionId, toolInput, previewContent) {
|
|
6827
|
+
async requestPermissionFromClient(params, toolName, signal, parentToolUseId, ownerSessionId, agentID, toolInput, previewContent) {
|
|
6323
6828
|
if (signal.aborted)
|
|
6324
6829
|
throw new Error("Tool use aborted");
|
|
6325
6830
|
// The SDK may invoke `canUseTool` (and therefore this permission request)
|
|
@@ -6337,7 +6842,7 @@ export class ClaudeAcpAgent {
|
|
|
6337
6842
|
// The request goes to a subagent's child session for a tool call of that
|
|
6338
6843
|
// subagent, but it blocks the turn of the owner session.
|
|
6339
6844
|
try {
|
|
6340
|
-
return await this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal));
|
|
6845
|
+
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, () => this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID));
|
|
6341
6846
|
}
|
|
6342
6847
|
catch (error) {
|
|
6343
6848
|
if (signal.aborted) {
|
|
@@ -6377,6 +6882,8 @@ export class ClaudeAcpAgent {
|
|
|
6377
6882
|
return;
|
|
6378
6883
|
}
|
|
6379
6884
|
session.emittedToolCalls.add(toolCallId);
|
|
6885
|
+
if (parentToolUseId)
|
|
6886
|
+
recordToolCallParent(session, toolCallId, parentToolUseId);
|
|
6380
6887
|
// SandboxNetworkAccess is a permission-only callback, not an executing
|
|
6381
6888
|
// tool: the SDK does not send a tool_result for its synthetic id.
|
|
6382
6889
|
if (!parentToolUseId &&
|
|
@@ -6401,6 +6908,7 @@ export class ClaudeAcpAgent {
|
|
|
6401
6908
|
session.emittedToolCalls.delete(toolCallId);
|
|
6402
6909
|
session.eagerToolCallSessions?.delete(toolCallId);
|
|
6403
6910
|
session.toolCallFields?.delete(toolCallId);
|
|
6911
|
+
session.toolCallParents?.delete(toolCallId);
|
|
6404
6912
|
throw error;
|
|
6405
6913
|
}
|
|
6406
6914
|
}
|
|
@@ -6510,7 +7018,7 @@ export class ClaudeAcpAgent {
|
|
|
6510
7018
|
// Like permission requests, the elicitation references this toolUseID, so
|
|
6511
7019
|
// make sure the tool_call has surfaced to the client before we send it.
|
|
6512
7020
|
await this.ensureToolCallEmitted(sessionId, toolName, toolUseID, toolInput, parentToolUseId, signal, permissionSessionId);
|
|
6513
|
-
return this.handleAskUserQuestion(sessionId, permissionSessionId, toolInput, toolUseID, signal);
|
|
7021
|
+
return this.handleAskUserQuestion(sessionId, permissionSessionId, toolInput, toolUseID, signal, agentID);
|
|
6514
7022
|
}
|
|
6515
7023
|
// Do not auto-allow here based on the session's advertised mode. Claude
|
|
6516
7024
|
// Code applies bypassPermissions before invoking canUseTool; a request
|
|
@@ -6579,7 +7087,7 @@ export class ClaudeAcpAgent {
|
|
|
6579
7087
|
...presentation,
|
|
6580
7088
|
options: permissionOptions,
|
|
6581
7089
|
sessionId: permissionSessionId,
|
|
6582
|
-
}, toolName, signal, parentToolUseId, sessionId, toolInput, previewContent);
|
|
7090
|
+
}, toolName, signal, parentToolUseId, sessionId, agentID, toolInput, previewContent);
|
|
6583
7091
|
if (signal.aborted)
|
|
6584
7092
|
throw new Error("Tool use aborted");
|
|
6585
7093
|
const decodedPermission = decodeClaudePermissionResponse(response, toolName, toolInput, toolUseID, permissionOptions, durableChangeSet);
|
|
@@ -6667,7 +7175,7 @@ export class ClaudeAcpAgent {
|
|
|
6667
7175
|
* The elicitation goes to `requestSessionId`, a subagent's child session for
|
|
6668
7176
|
* a question it asks, and blocks the turn of `sessionId`.
|
|
6669
7177
|
*/
|
|
6670
|
-
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID, signal) {
|
|
7178
|
+
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID, signal, agentID) {
|
|
6671
7179
|
const questions = extractAskUserQuestions(toolInput);
|
|
6672
7180
|
if (!questions) {
|
|
6673
7181
|
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
@@ -6675,7 +7183,7 @@ export class ClaudeAcpAgent {
|
|
|
6675
7183
|
const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
6676
7184
|
let response;
|
|
6677
7185
|
try {
|
|
6678
|
-
response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal));
|
|
7186
|
+
response = await this.withChildAwaitingUser(sessionId, requestSessionId, () => this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal), agentID));
|
|
6679
7187
|
}
|
|
6680
7188
|
catch (error) {
|
|
6681
7189
|
// A cancellation we requested (signal aborted) settles as an aborted tool
|
|
@@ -7338,28 +7846,23 @@ export class ClaudeAcpAgent {
|
|
|
7338
7846
|
// into the query.
|
|
7339
7847
|
const resolvedProvider = this.resolveProviderConfig();
|
|
7340
7848
|
const providerEnv = createEnvForProvider(resolvedProvider);
|
|
7341
|
-
const
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
const configuredSettingsObject = typeof configuredSettings === "string"
|
|
7349
|
-
? JSON.parse(await fs.readFile(path.resolve(params.cwd, configuredSettings), "utf8"))
|
|
7350
|
-
: configuredSettings;
|
|
7849
|
+
const providedSettings = userProvidedOptions?.settings;
|
|
7850
|
+
const providedSettingsObject = typeof providedSettings === "string"
|
|
7851
|
+
? JSON.parse(await fs.readFile(path.resolve(params.cwd, providedSettings), "utf8"))
|
|
7852
|
+
: providedSettings;
|
|
7853
|
+
const configuredSettingsObject = modelConfig
|
|
7854
|
+
? { ...modelConfig, ...providedSettingsObject }
|
|
7855
|
+
: providedSettingsObject;
|
|
7351
7856
|
// Claude Code applies env from settings.json after the subprocess env. Put
|
|
7352
7857
|
// an active ACP route in the programmatic settings tier too so user/project
|
|
7353
7858
|
// settings cannot silently restore a different ANTHROPIC_BASE_URL.
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
settings = {
|
|
7358
|
-
...baseSettings,
|
|
7859
|
+
const settings = resolvedProvider
|
|
7860
|
+
? {
|
|
7861
|
+
...configuredSettingsObject,
|
|
7359
7862
|
apiKeyHelper: "",
|
|
7360
|
-
env: { ...
|
|
7361
|
-
}
|
|
7362
|
-
|
|
7863
|
+
env: { ...configuredSettingsObject?.env, ...providerEnv },
|
|
7864
|
+
}
|
|
7865
|
+
: configuredSettingsObject;
|
|
7363
7866
|
const env = {
|
|
7364
7867
|
...process.env,
|
|
7365
7868
|
...userProvidedOptions?.env,
|
|
@@ -7551,6 +8054,8 @@ export class ClaudeAcpAgent {
|
|
|
7551
8054
|
prompt: input,
|
|
7552
8055
|
options,
|
|
7553
8056
|
});
|
|
8057
|
+
this.useAuthStatusSettings(settingsManager, configuredSettingsObject, userProvidedOptions?.env);
|
|
8058
|
+
const authSettingsEpoch = this.authEpoch;
|
|
7554
8059
|
timing.phase("prepare-query");
|
|
7555
8060
|
// `query()` spawns the CLI at once. Any throw between here and the
|
|
7556
8061
|
// registration in `this.sessions` would leave that child process running
|
|
@@ -7573,7 +8078,9 @@ export class ClaudeAcpAgent {
|
|
|
7573
8078
|
// Publish the identity BEFORE the guard can refuse this session. A
|
|
7574
8079
|
// refusal is exactly when the client most needs to know which account it
|
|
7575
8080
|
// was refused for.
|
|
7576
|
-
this.
|
|
8081
|
+
if (authSettingsEpoch === this.authEpoch) {
|
|
8082
|
+
this.publishSessionAccountIdentity(initializationResult.account, settingsManager.getApiKeyHelper(configuredSettingsObject));
|
|
8083
|
+
}
|
|
7577
8084
|
// Shared with the per-turn guard, so "warn once" spans the whole session.
|
|
7578
8085
|
const claudeSubscriptionGuard = {};
|
|
7579
8086
|
if (this.claudeSubscriptionGuardActive()) {
|
|
@@ -7775,7 +8282,7 @@ export class ClaudeAcpAgent {
|
|
|
7775
8282
|
messageIdToUuid: creationOpts.replayState?.messageIdToUuid ?? new Map(),
|
|
7776
8283
|
sessionFailureState: creationOpts.replayState?.sessionFailureState ?? createSessionFailureState(),
|
|
7777
8284
|
claudeSubscriptionGuard,
|
|
7778
|
-
accountKind: fromAccountInfo(initializationResult.account)?.kind,
|
|
8285
|
+
accountKind: fromAccountInfo(initializationResult.account, settingsManager.getApiKeyHelper(configuredSettingsObject))?.kind,
|
|
7779
8286
|
fileChangeReporter,
|
|
7780
8287
|
};
|
|
7781
8288
|
timing.phase("register");
|
|
@@ -7925,6 +8432,37 @@ function cancelledOutcome(session, turn) {
|
|
|
7925
8432
|
...(recorded._meta ? { _meta: recorded._meta } : {}),
|
|
7926
8433
|
};
|
|
7927
8434
|
}
|
|
8435
|
+
/** Whether the model request that `status` (a `status: requesting` frame)
|
|
8436
|
+
* starts may belong to the work a cancel stopped. It may while:
|
|
8437
|
+
* - a turn in {@link Session.cancelledPrompts} is unsettled: the stopped
|
|
8438
|
+
* turn, whose result is still to come;
|
|
8439
|
+
* - the orphan accounting still expects the result of an abandoned prompt
|
|
8440
|
+
* that Claude Code may still run, which it can fold into its own
|
|
8441
|
+
* follow-up after that follow-up's first request;
|
|
8442
|
+
* - the request serves one of {@link Session.cancelledPrompts}, as an
|
|
8443
|
+
* abandoned prompt's request does even when the interrupt receipt left it
|
|
8444
|
+
* out of the orphan accounting;
|
|
8445
|
+
* - the request serves an unsettled prompt. One that arrived while the cancel
|
|
8446
|
+
* awaited is recorded only when the cancel sweeps the queue, and its echo
|
|
8447
|
+
* ends the cancel right after its request when the cancel does not.
|
|
8448
|
+
* A prompt that is merely queued does not make another request belong to
|
|
8449
|
+
* the stopped work: a follow-up that Claude Code runs before a prompt sent
|
|
8450
|
+
* after the cancel names no prompt. */
|
|
8451
|
+
function servesCancelledWork(session, status) {
|
|
8452
|
+
const cancelled = session.cancelledPrompts;
|
|
8453
|
+
if (!cancelled)
|
|
8454
|
+
return false;
|
|
8455
|
+
const unsettled = (session.turnQueue ?? []).filter((turn) => !turn.settled);
|
|
8456
|
+
if (unsettled.some((turn) => cancelled.has(turn.promptUuid)))
|
|
8457
|
+
return true;
|
|
8458
|
+
if ((session.pendingOrphanResults ?? 0) > 0 || (session.orphanCommands?.size ?? 0) > 0) {
|
|
8459
|
+
return true;
|
|
8460
|
+
}
|
|
8461
|
+
const served = status.user_message_uuids;
|
|
8462
|
+
return (Array.isArray(served) &&
|
|
8463
|
+
served.some((uuid) => typeof uuid === "string" &&
|
|
8464
|
+
(cancelled.has(uuid) || unsettled.some((turn) => turn.promptUuid === uuid))));
|
|
8465
|
+
}
|
|
7928
8466
|
/** `_meta.quota` for a prompt response: what the turn spent, shaped like
|
|
7929
8467
|
* codex-acp's (snake_case container keys, camelCase counters) so a client
|
|
7930
8468
|
* reads one shape from either agent.
|
|
@@ -7937,16 +8475,88 @@ function cancelledOutcome(session, turn) {
|
|
|
7937
8475
|
* so the rows can total more than `token_count`. They are the fuller picture,
|
|
7938
8476
|
* not a decomposition of it. */
|
|
7939
8477
|
function turnQuotaMeta(session) {
|
|
8478
|
+
return quotaMeta(session.accumulatedUsage, session.accumulatedModelUsage);
|
|
8479
|
+
}
|
|
8480
|
+
/** `_meta.quota` for work that spent `usage` in its main agent loop and
|
|
8481
|
+
* `modelUsage` by model (see {@link turnQuotaMeta}). */
|
|
8482
|
+
function quotaMeta(usage, modelUsage) {
|
|
7940
8483
|
return {
|
|
7941
8484
|
quota: {
|
|
7942
|
-
token_count: quotaTokenCount(
|
|
7943
|
-
model_usage: Object.entries(
|
|
8485
|
+
token_count: quotaTokenCount(usage),
|
|
8486
|
+
model_usage: Object.entries(modelUsage ?? {}).map(([model, usage]) => ({
|
|
7944
8487
|
model,
|
|
7945
8488
|
token_count: quotaTokenCount(usage),
|
|
7946
8489
|
})),
|
|
7947
8490
|
},
|
|
7948
8491
|
};
|
|
7949
8492
|
}
|
|
8493
|
+
/** Whether a request that awaits the user for the subagent `agentID` (absent
|
|
8494
|
+
* for the main conversation) blocks the session's foreground work. The main
|
|
8495
|
+
* conversation's requests do. So do those of a subagent that runs in the
|
|
8496
|
+
* foreground of the main conversation, since the tool call that spawned it
|
|
8497
|
+
* waits on it, and so on up a chain of such subagents. A request of a
|
|
8498
|
+
* subagent in the background, or nested inside one, does not: the main
|
|
8499
|
+
* conversation goes on without it. Nor does one of an agent without a
|
|
8500
|
+
* record: a teammate, a settled subagent, or one that Claude Code left out
|
|
8501
|
+
* of its background tasks and the consumer pruned, which only happens to a
|
|
8502
|
+
* subagent nested in a background one. A subagent that the main
|
|
8503
|
+
* conversation waits on has a record by the time it asks, as its
|
|
8504
|
+
* `task_started` comes first; when the consumer reads the request first, the
|
|
8505
|
+
* `task_started` checks again. */
|
|
8506
|
+
function blocksForeground(session, agentID) {
|
|
8507
|
+
const seen = new Set();
|
|
8508
|
+
for (let id = agentID; id !== undefined && !seen.has(id);) {
|
|
8509
|
+
seen.add(id);
|
|
8510
|
+
const task = session.liveBackgroundTasks.get(id);
|
|
8511
|
+
if (!task)
|
|
8512
|
+
return false;
|
|
8513
|
+
if (task.backgrounded)
|
|
8514
|
+
return false;
|
|
8515
|
+
id = task.parentTaskId;
|
|
8516
|
+
}
|
|
8517
|
+
return true;
|
|
8518
|
+
}
|
|
8519
|
+
/** Whether Claude Code ended the work of `result` because an interrupt
|
|
8520
|
+
* stopped it, while the model streamed or while a tool ran. */
|
|
8521
|
+
function isAbortedResult(result) {
|
|
8522
|
+
return (result.terminal_reason === "aborted_streaming" || result.terminal_reason === "aborted_tools");
|
|
8523
|
+
}
|
|
8524
|
+
/** How a model cycle that Claude Code started on its own ended, read from its
|
|
8525
|
+
* result as a turn's is: a refusal, the token limit, and the turn and budget
|
|
8526
|
+
* limits are their stop reasons, an error result fails the cycle, and a
|
|
8527
|
+
* cancel ends it `cancelled`. The usage and `_meta.quota` are the result's
|
|
8528
|
+
* own, `modelUsage` its increment by model, since autonomous results stay out
|
|
8529
|
+
* of the turn tally. */
|
|
8530
|
+
function autonomousCycleEnd(result, modelUsage, cancelled) {
|
|
8531
|
+
// Typed non-null, but third-party backends have been observed omitting
|
|
8532
|
+
// usage token fields (see snapshotFromUsage).
|
|
8533
|
+
const tally = {
|
|
8534
|
+
inputTokens: result.usage.input_tokens ?? 0,
|
|
8535
|
+
outputTokens: result.usage.output_tokens ?? 0,
|
|
8536
|
+
cachedReadTokens: result.usage.cache_read_input_tokens ?? 0,
|
|
8537
|
+
cachedWriteTokens: result.usage.cache_creation_input_tokens ?? 0,
|
|
8538
|
+
};
|
|
8539
|
+
const ended = (stopReason) => ({
|
|
8540
|
+
outcome: {
|
|
8541
|
+
stopReason,
|
|
8542
|
+
usage: { ...tally, totalTokens: tallyTotal(tally) },
|
|
8543
|
+
_meta: quotaMeta(tally, modelUsage),
|
|
8544
|
+
},
|
|
8545
|
+
});
|
|
8546
|
+
if (cancelled)
|
|
8547
|
+
return ended("cancelled");
|
|
8548
|
+
if (result.stop_reason === "refusal")
|
|
8549
|
+
return ended("refusal");
|
|
8550
|
+
if (result.stop_reason === "max_tokens")
|
|
8551
|
+
return ended("max_tokens");
|
|
8552
|
+
if (result.is_error) {
|
|
8553
|
+
const text = result.subtype === "success" ? result.result : result.errors.join(", ") || result.subtype;
|
|
8554
|
+
return { error: RequestError.internalError(undefined, text) };
|
|
8555
|
+
}
|
|
8556
|
+
return ended(result.subtype === "success" || result.subtype === "error_during_execution"
|
|
8557
|
+
? "end_turn"
|
|
8558
|
+
: "max_turn_requests");
|
|
8559
|
+
}
|
|
7950
8560
|
/** One `token_count` object. `cachedInputTokens` is cache reads, matching
|
|
7951
8561
|
* codex's field; Claude also reports cache writes, which codex has no slot
|
|
7952
8562
|
* for, so those ride along in an extra sibling (the same name the ACP `usage`
|
|
@@ -8577,6 +9187,18 @@ function isTaskTool(toolName) {
|
|
|
8577
9187
|
function shouldEmitToolCall(toolName) {
|
|
8578
9188
|
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
|
8579
9189
|
}
|
|
9190
|
+
const MAX_TOOL_CALL_PARENTS = 1000;
|
|
9191
|
+
/** Records the Agent/Task call that started the subagent making `toolCallId`. */
|
|
9192
|
+
function recordToolCallParent(session, toolCallId, parentToolUseId) {
|
|
9193
|
+
const parents = (session.toolCallParents ??= new Map());
|
|
9194
|
+
parents.delete(toolCallId);
|
|
9195
|
+
parents.set(toolCallId, parentToolUseId);
|
|
9196
|
+
if (parents.size > MAX_TOOL_CALL_PARENTS) {
|
|
9197
|
+
const oldest = parents.keys().next().value;
|
|
9198
|
+
if (oldest !== undefined)
|
|
9199
|
+
parents.delete(oldest);
|
|
9200
|
+
}
|
|
9201
|
+
}
|
|
8580
9202
|
const MAX_DISPATCHED_TOOL_CALLS = 1000;
|
|
8581
9203
|
/** Records a tool call that Claude Code sent to the tool runner. */
|
|
8582
9204
|
function recordDispatchedToolCall(session, toolCallId) {
|
|
@@ -8689,6 +9311,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8689
9311
|
let update = null;
|
|
8690
9312
|
// The tool call that this chunk finishes, if it is a tool result.
|
|
8691
9313
|
let finishedToolCallId;
|
|
9314
|
+
// The plan that this chunk approves, if it is an ExitPlanMode result.
|
|
9315
|
+
let approvedPlanUpdate;
|
|
8692
9316
|
switch (chunk.type) {
|
|
8693
9317
|
case "text":
|
|
8694
9318
|
case "text_delta": {
|
|
@@ -8846,7 +9470,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8846
9470
|
update: {
|
|
8847
9471
|
toolCallId: chunk.tool_use_id,
|
|
8848
9472
|
sessionUpdate: "tool_call_update",
|
|
8849
|
-
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution),
|
|
9473
|
+
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution, { cancelled: options?.cancelled }),
|
|
8850
9474
|
rawOutput: chunk.content,
|
|
8851
9475
|
},
|
|
8852
9476
|
});
|
|
@@ -8875,7 +9499,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8875
9499
|
},
|
|
8876
9500
|
toolCallId: chunk.tool_use_id,
|
|
8877
9501
|
sessionUpdate: "tool_call_update",
|
|
8878
|
-
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution),
|
|
9502
|
+
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution, { cancelled: options?.cancelled }),
|
|
8879
9503
|
rawOutput: chunk.content,
|
|
8880
9504
|
},
|
|
8881
9505
|
});
|
|
@@ -8927,6 +9551,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8927
9551
|
.result(toolUse, chunk, {
|
|
8928
9552
|
structured: toolUseResult,
|
|
8929
9553
|
nonExecution,
|
|
9554
|
+
cancelled: options?.cancelled,
|
|
8930
9555
|
})
|
|
8931
9556
|
.reverse();
|
|
8932
9557
|
for (const outputUpdate of rest.reverse()) {
|
|
@@ -8935,6 +9560,19 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8935
9560
|
}
|
|
8936
9561
|
output.push({ sessionId, update: outputUpdate });
|
|
8937
9562
|
}
|
|
9563
|
+
// An approved plan of the main conversation becomes the session's
|
|
9564
|
+
// markdown plan, after the tool call's result. An approval that
|
|
9565
|
+
// clears the context ends the tool with an error result instead, and
|
|
9566
|
+
// the restart into the fresh context publishes that plan (see
|
|
9567
|
+
// ExitPlanCoordinator.restart).
|
|
9568
|
+
if (toolUse.name === "ExitPlanMode" &&
|
|
9569
|
+
options?.clientCapabilities?.plan &&
|
|
9570
|
+
!options.parentToolUseId &&
|
|
9571
|
+
!("is_error" in chunk && chunk.is_error)) {
|
|
9572
|
+
const plan = approvedPlan(toolUse.input, toolUseResult);
|
|
9573
|
+
if (plan)
|
|
9574
|
+
approvedPlanUpdate = planModeUpdate(plan);
|
|
9575
|
+
}
|
|
8938
9576
|
update = finalUpdate;
|
|
8939
9577
|
}
|
|
8940
9578
|
// The tool_use is fully resolved now — drop it so a long session doesn't
|
|
@@ -8974,6 +9612,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8974
9612
|
output.push({ sessionId, update });
|
|
8975
9613
|
}
|
|
8976
9614
|
}
|
|
9615
|
+
if (approvedPlanUpdate)
|
|
9616
|
+
output.push({ sessionId, update: approvedPlanUpdate });
|
|
8977
9617
|
if (finishedToolCallId !== undefined) {
|
|
8978
9618
|
// The PostToolUse hook can still send the final diff, so the fields
|
|
8979
9619
|
// stay tracked until its callback leaves the registry.
|