@agentclientprotocol/claude-agent-acp 0.89.2-preview.8 → 0.90.0
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 +52 -3
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +507 -139
- 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 +30 -9
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +48 -22
- package/dist/tool-calls/reporters/bash.d.ts +7 -0
- package/dist/tool-calls/reporters/bash.d.ts.map +1 -1
- package/dist/tool-calls/reporters/bash.js +16 -1
- package/dist/tool-calls/reporters/search.d.ts.map +1 -1
- package/dist/tool-calls/reporters/search.js +20 -2
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +45 -12
- package/dist/v2/initialize.d.ts.map +1 -1
- package/dist/v2/initialize.js +10 -1
- 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 +9 -9
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";
|
|
@@ -548,28 +549,6 @@ function subagentLaunchIds(content) {
|
|
|
548
549
|
? [block.id]
|
|
549
550
|
: []);
|
|
550
551
|
}
|
|
551
|
-
function replaySubagentTerminalState(block) {
|
|
552
|
-
if (block.is_error !== true)
|
|
553
|
-
return "completed";
|
|
554
|
-
const text = replayContentText(block.content).toLowerCase();
|
|
555
|
-
return /\b(?:cancelled|canceled|interrupted|stopped|killed)\b/.test(text)
|
|
556
|
-
? "cancelled"
|
|
557
|
-
: "failed";
|
|
558
|
-
}
|
|
559
|
-
function replayContentText(value, seen = new Set()) {
|
|
560
|
-
if (typeof value === "string")
|
|
561
|
-
return value;
|
|
562
|
-
if (typeof value !== "object" || value === null || seen.has(value))
|
|
563
|
-
return "";
|
|
564
|
-
seen.add(value);
|
|
565
|
-
if (Array.isArray(value))
|
|
566
|
-
return value.map((item) => replayContentText(item, seen)).join(" ");
|
|
567
|
-
const record = value;
|
|
568
|
-
return [record.text, record.content, record.message]
|
|
569
|
-
.map((item) => replayContentText(item, seen))
|
|
570
|
-
.filter(Boolean)
|
|
571
|
-
.join(" ");
|
|
572
|
-
}
|
|
573
552
|
function stripSubagentTextAndThinking(content) {
|
|
574
553
|
if (!Array.isArray(content))
|
|
575
554
|
return content;
|
|
@@ -1172,6 +1151,8 @@ export class ClaudeAcpAgent {
|
|
|
1172
1151
|
/** Last auth identity reported to the client, connection-scoped like
|
|
1173
1152
|
* `authenticate`/`logout`. Undefined means "not determined yet". */
|
|
1174
1153
|
currentAuthStatus;
|
|
1154
|
+
authStatusSettings;
|
|
1155
|
+
authStatusHelper;
|
|
1175
1156
|
/** In-flight `claude auth status --json` probe, shared by every caller so
|
|
1176
1157
|
* a concurrent `initialize` and start-of-prompt read never spawn two CLI
|
|
1177
1158
|
* processes. */
|
|
@@ -1232,6 +1213,7 @@ export class ClaudeAcpAgent {
|
|
|
1232
1213
|
},
|
|
1233
1214
|
applyFastMode: (session, enabled) => this.applyFastMode(session, enabled),
|
|
1234
1215
|
sessionUpdate: (notification) => this.client.sessionUpdate(notification),
|
|
1216
|
+
supportsPlanOperations: () => this.clientCapabilities?.plan != null,
|
|
1235
1217
|
ensureConsumer: (session, id) => this.ensureConsumer(session, id),
|
|
1236
1218
|
logError: (message, error) => this.logger.error(message, error),
|
|
1237
1219
|
destroyReplacement: (id, session) => {
|
|
@@ -1378,6 +1360,8 @@ export class ClaudeAcpAgent {
|
|
|
1378
1360
|
terminalAuthMethods.push(consoleLoginMethod);
|
|
1379
1361
|
}
|
|
1380
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`).
|
|
1381
1365
|
const sessionCapabilities = {
|
|
1382
1366
|
additionalDirectories: {},
|
|
1383
1367
|
close: {},
|
|
@@ -1385,7 +1369,7 @@ export class ClaudeAcpAgent {
|
|
|
1385
1369
|
fork: {},
|
|
1386
1370
|
list: {},
|
|
1387
1371
|
resume: {},
|
|
1388
|
-
subagents: {},
|
|
1372
|
+
...(this.toolCallCapabilities.air.client ? { subagents: {} } : {}),
|
|
1389
1373
|
};
|
|
1390
1374
|
return {
|
|
1391
1375
|
protocolVersion: 1,
|
|
@@ -1506,14 +1490,30 @@ export class ClaudeAcpAgent {
|
|
|
1506
1490
|
configOptions: response.configOptions,
|
|
1507
1491
|
};
|
|
1508
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
|
+
*/
|
|
1509
1500
|
async unstable_forkSession(params) {
|
|
1510
1501
|
if (this.providerUpdate)
|
|
1511
1502
|
await this.providerUpdate;
|
|
1512
|
-
|
|
1503
|
+
const { sessionId } = await forkSession(params, {
|
|
1513
1504
|
liveMessageIdToUuid: this.sessions[params.sessionId]?.messageIdToUuid,
|
|
1514
1505
|
logger: this.logger,
|
|
1515
1506
|
messageIdForGrouping,
|
|
1516
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 };
|
|
1517
1517
|
}
|
|
1518
1518
|
async resumeSession(params) {
|
|
1519
1519
|
if (this.providerUpdate)
|
|
@@ -1682,7 +1682,22 @@ export class ClaudeAcpAgent {
|
|
|
1682
1682
|
* under an apiKeyHelper) means "nothing to add", not "logged out" — keep
|
|
1683
1683
|
* what the CLI probe already established instead of overwriting it.
|
|
1684
1684
|
*/
|
|
1685
|
-
|
|
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) {
|
|
1686
1701
|
if (this.providerConfig) {
|
|
1687
1702
|
if (!this.loggedOverriddenAccount) {
|
|
1688
1703
|
this.loggedOverriddenAccount = true;
|
|
@@ -1693,7 +1708,7 @@ export class ClaudeAcpAgent {
|
|
|
1693
1708
|
if (this.gatewayAuthRequest) {
|
|
1694
1709
|
return;
|
|
1695
1710
|
}
|
|
1696
|
-
const fromSession = fromAccountInfo(account);
|
|
1711
|
+
const fromSession = fromAccountInfo(account, apiKeyHelper);
|
|
1697
1712
|
if (fromSession) {
|
|
1698
1713
|
this.setAuthStatus(fromSession);
|
|
1699
1714
|
}
|
|
@@ -1726,6 +1741,8 @@ export class ClaudeAcpAgent {
|
|
|
1726
1741
|
// auth-affecting event describes a world that no longer exists. Remember
|
|
1727
1742
|
// which one this read belongs to and drop the answer if it moved on.
|
|
1728
1743
|
const epoch = this.authEpoch;
|
|
1744
|
+
const context = this.authStatusSettings;
|
|
1745
|
+
const apiKeyHelper = context?.manager.getApiKeyHelper(context.override);
|
|
1729
1746
|
// ACP gateway auth bypasses the CLI credential store entirely, so the
|
|
1730
1747
|
// probe would report an identity that is not the one being used. A mere
|
|
1731
1748
|
// client provider override (`providers/set`) does NOT skip the probe: it
|
|
@@ -1737,8 +1754,13 @@ export class ClaudeAcpAgent {
|
|
|
1737
1754
|
let stdout;
|
|
1738
1755
|
try {
|
|
1739
1756
|
const cliPath = await claudeCliPath();
|
|
1740
|
-
|
|
1757
|
+
const args = ["auth", "status", "--json"];
|
|
1758
|
+
if (context?.override)
|
|
1759
|
+
args.push("--settings", JSON.stringify(context.override));
|
|
1760
|
+
({ stdout } = await execFileAsync(cliPath, args, {
|
|
1741
1761
|
timeout: AUTH_STATUS_PROBE_TIMEOUT_MS,
|
|
1762
|
+
cwd: context?.manager.getCwd() ?? process.cwd(),
|
|
1763
|
+
env: context?.env ? { ...process.env, ...context.env } : process.env,
|
|
1742
1764
|
}));
|
|
1743
1765
|
}
|
|
1744
1766
|
catch (error) {
|
|
@@ -1761,7 +1783,16 @@ export class ClaudeAcpAgent {
|
|
|
1761
1783
|
}
|
|
1762
1784
|
stdout = failed.stdout;
|
|
1763
1785
|
}
|
|
1764
|
-
|
|
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
|
+
}
|
|
1765
1796
|
if (!status) {
|
|
1766
1797
|
this.logger.error("claude auth status returned unparseable output");
|
|
1767
1798
|
return undefined;
|
|
@@ -2027,6 +2058,7 @@ export class ClaudeAcpAgent {
|
|
|
2027
2058
|
// that finds the new credential must not be the one the refusal skipped.
|
|
2028
2059
|
// The guard itself consumes the result only at the next turn boundary (see
|
|
2029
2060
|
// `markSessionsWhoseAccountKindChanged`).
|
|
2061
|
+
this.useAuthStatusSettings(session.settingsManager, session.effortSettingsOverride, session.creationParams?._meta?.claudeCode?.options?.env);
|
|
2030
2062
|
void this.probeCliAuthStatus();
|
|
2031
2063
|
const signOutRespawn = this.respawnSignedOutSession(params.sessionId, session);
|
|
2032
2064
|
if (signOutRespawn)
|
|
@@ -2245,7 +2277,7 @@ export class ClaudeAcpAgent {
|
|
|
2245
2277
|
logger: this.logger,
|
|
2246
2278
|
// The guard reads the account anyway; reuse that read to keep the
|
|
2247
2279
|
// reported identity current, refusal or not.
|
|
2248
|
-
onAccount: (account) => this.publishSessionAccountIdentity(account),
|
|
2280
|
+
onAccount: (account) => this.publishSessionAccountIdentity(account, session.settingsManager.getApiKeyHelper(session.effortSettingsOverride)),
|
|
2249
2281
|
});
|
|
2250
2282
|
}
|
|
2251
2283
|
async goal(params) {
|
|
@@ -2500,6 +2532,10 @@ export class ClaudeAcpAgent {
|
|
|
2500
2532
|
streamedBlocksByParent.set(key, (blocks = []));
|
|
2501
2533
|
return blocks;
|
|
2502
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();
|
|
2503
2539
|
// A client gets the consolidated subagent text when it negotiated the
|
|
2504
2540
|
// transcript extension or the `forwardSubagentText` session option. AIR
|
|
2505
2541
|
// also gets it with native subagent sessions, and AIR gets no streamed
|
|
@@ -2528,7 +2564,7 @@ export class ClaudeAcpAgent {
|
|
|
2528
2564
|
* recognizable by the `parentToolUseId` meta that toAcpNotifications
|
|
2529
2565
|
* stamps from `parent_tool_use_id`, and never reach the top-level feed
|
|
2530
2566
|
* as the turn's answer. */
|
|
2531
|
-
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));
|
|
2532
2568
|
// Adapter-composed advisories (hook feedback, model fallbacks) are live
|
|
2533
2569
|
// events, not something the model said. Clients on the notice contract get
|
|
2534
2570
|
// them as `notice` updates; the rest keep the bold-label transcript line.
|
|
@@ -2563,6 +2599,19 @@ export class ClaudeAcpAgent {
|
|
|
2563
2599
|
const eagerOwnerSessionId = toolCallId
|
|
2564
2600
|
? session.eagerToolCallSessions?.get(toolCallId)
|
|
2565
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
|
+
}
|
|
2566
2615
|
const routedNotification = await subagents.route(notification, sendUpdate, eagerOwnerSessionId);
|
|
2567
2616
|
if (!routedNotification) {
|
|
2568
2617
|
// Native Agent/Task control calls are intentionally not transcript
|
|
@@ -3011,7 +3060,35 @@ export class ClaudeAcpAgent {
|
|
|
3011
3060
|
}
|
|
3012
3061
|
this.syncAwaitingUser(params.sessionId, session);
|
|
3013
3062
|
};
|
|
3014
|
-
|
|
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);
|
|
3091
|
+
};
|
|
3015
3092
|
/** Registers a settled subagent again when the SDK resumes it without a
|
|
3016
3093
|
* new `task_started`. The active turn is the one that resumed it, so it
|
|
3017
3094
|
* holds until the subagent settles again. A repeated resume signal finds
|
|
@@ -3082,6 +3159,92 @@ export class ClaudeAcpAgent {
|
|
|
3082
3159
|
await settleActive(outcome);
|
|
3083
3160
|
}
|
|
3084
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
|
+
* Runs once per turn: a second call, from the other settling path, waits
|
|
3195
|
+
* for the first (see `Turn.closingCancelledToolCalls`). */
|
|
3196
|
+
const closeCancelledToolCalls = (turn) => (turn.closingCancelledToolCalls ??= (async () => {
|
|
3197
|
+
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
|
3198
|
+
for (const toolCallId of [...session.emittedToolCalls]) {
|
|
3199
|
+
const parentToolUseId = session.toolCallParents?.get(toolCallId);
|
|
3200
|
+
if (parentToolUseId !== undefined && runsInBackground(parentToolUseId))
|
|
3201
|
+
continue;
|
|
3202
|
+
const report = renderer.unfinished({
|
|
3203
|
+
toolCallId,
|
|
3204
|
+
ran: session.dispatchedToolCalls?.has(toolCallId) === true,
|
|
3205
|
+
cancelled: true,
|
|
3206
|
+
});
|
|
3207
|
+
// Named as the call's other updates are: the subagent runtime routes the
|
|
3208
|
+
// report with them, to the child session, or to none that has ended.
|
|
3209
|
+
if (parentToolUseId !== undefined)
|
|
3210
|
+
stampParentToolUseId(report, parentToolUseId);
|
|
3211
|
+
await closeToolCall(toolCallId, report);
|
|
3212
|
+
}
|
|
3213
|
+
})());
|
|
3214
|
+
session.closeCancelledToolCalls = closeCancelledToolCalls;
|
|
3215
|
+
/** Delivers the results that the work a cancel stopped sends for tool calls
|
|
3216
|
+
* that the client holds open. Claude Code's interrupt ends each running tool
|
|
3217
|
+
* of the main conversation with an error result, before the result that
|
|
3218
|
+
* ends the turn. The tool call gets that result like any other, so its
|
|
3219
|
+
* final status and `nonExecutionKind` reach the client. Nothing else of the
|
|
3220
|
+
* message is applied: the other effects of a tool result (async tasks, plan
|
|
3221
|
+
* state, the ExitPlanMode flow) belong to the stopped work, which stays
|
|
3222
|
+
* dropped. */
|
|
3223
|
+
const sendInterruptedToolResults = async (message) => {
|
|
3224
|
+
const content = message.message.content;
|
|
3225
|
+
if (!Array.isArray(content))
|
|
3226
|
+
return;
|
|
3227
|
+
const results = content.filter((block) => block.type === "tool_result" && session.emittedToolCalls.has(block.tool_use_id));
|
|
3228
|
+
if (results.length === 0)
|
|
3229
|
+
return;
|
|
3230
|
+
const resultCount = content.filter((block) => block.type === "tool_result").length;
|
|
3231
|
+
for (const notification of toAcpNotifications(results, "user", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
3232
|
+
clientCapabilities: this.clientCapabilities,
|
|
3233
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
3234
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
3235
|
+
cwd: session.cwd,
|
|
3236
|
+
taskState: session.taskState,
|
|
3237
|
+
emittedToolCalls: session.emittedToolCalls,
|
|
3238
|
+
toolCallFields: toolCallFieldsOf(session),
|
|
3239
|
+
messageId: messageIdForGrouping(message),
|
|
3240
|
+
// `tool_use_result` describes the one result of its message.
|
|
3241
|
+
toolUseResult: results.length === resultCount ? message.tool_use_result : undefined,
|
|
3242
|
+
toolResultMeta: message.tool_result_meta,
|
|
3243
|
+
cancelled: true,
|
|
3244
|
+
})) {
|
|
3245
|
+
await sendUpdate(notification);
|
|
3246
|
+
}
|
|
3247
|
+
};
|
|
3085
3248
|
/** At the actual turn boundary, preview its checkpoint, settle the active
|
|
3086
3249
|
* turn exactly once, disarm the force-cancel backstop, and drop it from
|
|
3087
3250
|
* the queue. Cancellation and provider failures skip checkpoint I/O. */
|
|
@@ -3107,16 +3270,7 @@ export class ClaudeAcpAgent {
|
|
|
3107
3270
|
* ended meanwhile.
|
|
3108
3271
|
*/
|
|
3109
3272
|
const closeUnfinished = async (toolCallId, ran) => {
|
|
3110
|
-
|
|
3111
|
-
unregisterHookCallback(toolCallId);
|
|
3112
|
-
session.emittedToolCalls.delete(toolCallId);
|
|
3113
|
-
delete session.toolUseCache[toolCallId];
|
|
3114
|
-
session.toolCallFields?.delete(toolCallId);
|
|
3115
|
-
session.dispatchedToolCalls?.delete(toolCallId);
|
|
3116
|
-
await sendUpdate({
|
|
3117
|
-
sessionId: params.sessionId,
|
|
3118
|
-
update: renderer.unfinished({ toolCallId, ran }),
|
|
3119
|
-
});
|
|
3273
|
+
await closeToolCall(toolCallId, renderer.unfinished({ toolCallId, ran }));
|
|
3120
3274
|
// Cancellation can arrive while we await the client update.
|
|
3121
3275
|
if (turn.settled || session.activeTurn !== turn)
|
|
3122
3276
|
return false;
|
|
@@ -3149,6 +3303,15 @@ export class ClaudeAcpAgent {
|
|
|
3149
3303
|
return;
|
|
3150
3304
|
}
|
|
3151
3305
|
}
|
|
3306
|
+
// The client is told that the work a cancel stopped ended (the prompt
|
|
3307
|
+
// response on v1, `idle` on v2) only after every tool call that work
|
|
3308
|
+
// opened has ended.
|
|
3309
|
+
if (result.stopReason === "cancelled" && session.cancelled) {
|
|
3310
|
+
await closeCancelledToolCalls(turn);
|
|
3311
|
+
// `cancelTurns` can settle the turn while the reports are in flight.
|
|
3312
|
+
if (turn.settled || session.activeTurn !== turn)
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3152
3315
|
turn.settling = true;
|
|
3153
3316
|
turn.settlingOutcome = result;
|
|
3154
3317
|
if (reportReason === "notReported") {
|
|
@@ -4059,9 +4222,14 @@ export class ClaudeAcpAgent {
|
|
|
4059
4222
|
this.logger.log(`Ignoring task_notification for task ${message.task_id} from earlier run ${message.run_id}`);
|
|
4060
4223
|
break;
|
|
4061
4224
|
}
|
|
4225
|
+
// In the RFD form, a resumed child runs under the call that
|
|
4226
|
+
// resumed it, and the end of an earlier delegation ends
|
|
4227
|
+
// nothing: the child and its live task belong to the new run.
|
|
4228
|
+
if (subagents.isStaleEnd(message.task_id, message.tool_use_id))
|
|
4229
|
+
break;
|
|
4062
4230
|
// The task settled — no further tool calls can originate
|
|
4063
4231
|
// from it, so its registry entry can be dropped.
|
|
4064
|
-
await subagents.finishTask(message.task_id, message.status, sendUpdate, message.tool_use_id);
|
|
4232
|
+
await subagents.finishTask(message.task_id, message.status, sendUpdate, message.tool_use_id, { reason: message.reason });
|
|
4065
4233
|
await asyncTasks.taskNotification({
|
|
4066
4234
|
task_id: message.task_id,
|
|
4067
4235
|
status: message.status,
|
|
@@ -4073,7 +4241,9 @@ export class ClaudeAcpAgent {
|
|
|
4073
4241
|
subagents.discardPending(message.tool_use_id);
|
|
4074
4242
|
// The subagent streams no more text under this parent.
|
|
4075
4243
|
streamedBlocksByParent.delete(message.tool_use_id);
|
|
4244
|
+
droppedStreams.delete(message.tool_use_id);
|
|
4076
4245
|
}
|
|
4246
|
+
await endBackgroundTerminal(message.task_id);
|
|
4077
4247
|
settleLiveTask(message.task_id);
|
|
4078
4248
|
break;
|
|
4079
4249
|
case "task_updated":
|
|
@@ -4098,10 +4268,13 @@ export class ClaudeAcpAgent {
|
|
|
4098
4268
|
if (message.patch.status === "completed" ||
|
|
4099
4269
|
message.patch.status === "failed" ||
|
|
4100
4270
|
message.patch.status === "killed") {
|
|
4101
|
-
await subagents.finishTask(message.task_id, message.patch.status, sendUpdate);
|
|
4271
|
+
await subagents.finishTask(message.task_id, message.patch.status, sendUpdate, undefined, { error: message.patch.status === "failed" ? message.patch.error : undefined });
|
|
4102
4272
|
const parentToolUseId = session.liveBackgroundTasks.get(message.task_id)?.parentToolUseId;
|
|
4103
|
-
if (parentToolUseId)
|
|
4273
|
+
if (parentToolUseId) {
|
|
4104
4274
|
streamedBlocksByParent.delete(parentToolUseId);
|
|
4275
|
+
droppedStreams.delete(parentToolUseId);
|
|
4276
|
+
}
|
|
4277
|
+
await endBackgroundTerminal(message.task_id);
|
|
4105
4278
|
settleLiveTask(message.task_id);
|
|
4106
4279
|
}
|
|
4107
4280
|
else if (message.patch.status === "running" ||
|
|
@@ -4925,43 +5098,14 @@ export class ClaudeAcpAgent {
|
|
|
4925
5098
|
// reset, and the synthetic-auth/system/local-command paths
|
|
4926
5099
|
// `break` early too. Block indices restart at 0 each message, so
|
|
4927
5100
|
// leftover entries would otherwise collide with this message's
|
|
4928
|
-
// blocks and re-emit (or truncate) already-streamed text.
|
|
4929
|
-
//
|
|
4930
|
-
//
|
|
4931
|
-
//
|
|
4932
|
-
//
|
|
5101
|
+
// blocks and re-emit (or truncate) already-streamed text. Keyed by
|
|
5102
|
+
// the stream, so a subagent's message can't clear the top-level
|
|
5103
|
+
// record. Fires once, before any of this message's blocks, so it
|
|
5104
|
+
// doesn't disturb the mid-message turn-activation path the way
|
|
5105
|
+
// resetting on turn activation would. The record's drop latch
|
|
5106
|
+
// (see below) resets with it.
|
|
4933
5107
|
streamedBlocksOf(message.parent_tool_use_id).length = 0;
|
|
4934
|
-
|
|
4935
|
-
// Accumulate the text/thinking actually streamed live, so the
|
|
4936
|
-
// `assistant` case below can diff its assembled blocks against what
|
|
4937
|
-
// already reached the client as chunks and forward only the
|
|
4938
|
-
// remainder. Each stream (top level or one subagent) keeps its own
|
|
4939
|
-
// record, so a subagent cannot attribute content to another stream.
|
|
4940
|
-
// Contiguous deltas of the same block (same index and type) extend
|
|
4941
|
-
// the current entry; anything else opens a new one.
|
|
4942
|
-
if (message.event.type === "content_block_delta") {
|
|
4943
|
-
const delta = message.event.delta;
|
|
4944
|
-
const chunk = delta.type === "text_delta"
|
|
4945
|
-
? { type: "text", text: delta.text }
|
|
4946
|
-
: delta.type === "thinking_delta"
|
|
4947
|
-
? { type: "thinking", text: delta.thinking }
|
|
4948
|
-
: undefined;
|
|
4949
|
-
// Skip empty deltas (some gateways emit empty thinking chunks —
|
|
4950
|
-
// #793): appending "" is a no-op, but pushing a "" entry would
|
|
4951
|
-
// create a block the consolidated handler's `text.length > 0`
|
|
4952
|
-
// guard can never consume, stalling the diff cursor and
|
|
4953
|
-
// re-emitting the next block as a duplicate.
|
|
4954
|
-
if (chunk?.text) {
|
|
4955
|
-
const index = message.event.index;
|
|
4956
|
-
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
4957
|
-
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
4958
|
-
if (last && last.index === index && last.type === chunk.type) {
|
|
4959
|
-
last.text += chunk.text;
|
|
4960
|
-
}
|
|
4961
|
-
else {
|
|
4962
|
-
streamedBlocks.push({ index, type: chunk.type, text: chunk.text });
|
|
4963
|
-
}
|
|
4964
|
-
}
|
|
5108
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
4965
5109
|
}
|
|
4966
5110
|
if (message.parent_tool_use_id === null &&
|
|
4967
5111
|
(message.event.type === "message_start" || message.event.type === "message_delta")) {
|
|
@@ -5015,7 +5159,71 @@ export class ClaudeAcpAgent {
|
|
|
5015
5159
|
});
|
|
5016
5160
|
}
|
|
5017
5161
|
}
|
|
5018
|
-
|
|
5162
|
+
// After a cancel, output streams to the client until the client is
|
|
5163
|
+
// told that the stopped work ended (see workEndReported), so what
|
|
5164
|
+
// the interrupt flushes before then still goes out, as both
|
|
5165
|
+
// protocol versions allow. After that, while the window after the
|
|
5166
|
+
// cancel is open, streamed text, thinking and tool calls are
|
|
5167
|
+
// dropped like the consolidated messages they belong to (see the
|
|
5168
|
+
// `cancelled` check in the user/assistant case). Otherwise the
|
|
5169
|
+
// answer to a prompt the cancel abandoned, which Claude Code had
|
|
5170
|
+
// already taken off its queue, would stream to the client after the
|
|
5171
|
+
// cancel was reported.
|
|
5172
|
+
//
|
|
5173
|
+
// Where the consolidated message delivers the text (the same
|
|
5174
|
+
// condition as in the user/assistant case), a stream that loses a
|
|
5175
|
+
// delta keeps dropping until the record below resets: at the next
|
|
5176
|
+
// message, or when a consolidated message consumes the record. If
|
|
5177
|
+
// the next prompt's echo ends the window mid-block, the record then
|
|
5178
|
+
// stays empty and the consolidated message delivers the block's
|
|
5179
|
+
// whole text once. A record holding only the block's tail is not a
|
|
5180
|
+
// prefix of the text, so the text would be sent again (see
|
|
5181
|
+
// unstreamedRemainder).
|
|
5182
|
+
const streamKey = message.parent_tool_use_id ?? "";
|
|
5183
|
+
if (session.cancelled && this.workEndReported(params.sessionId, session)) {
|
|
5184
|
+
if (message.parent_tool_use_id === null || forwardsSubagentText()) {
|
|
5185
|
+
droppedStreams.add(streamKey);
|
|
5186
|
+
}
|
|
5187
|
+
break;
|
|
5188
|
+
}
|
|
5189
|
+
if (droppedStreams.has(streamKey)) {
|
|
5190
|
+
break;
|
|
5191
|
+
}
|
|
5192
|
+
// Accumulate the text/thinking actually streamed live, so the
|
|
5193
|
+
// `assistant` case below can diff its assembled blocks against what
|
|
5194
|
+
// already reached the client as chunks and forward only the
|
|
5195
|
+
// remainder. Each stream (top level or one subagent) keeps its own
|
|
5196
|
+
// record, so a subagent cannot attribute content to another stream.
|
|
5197
|
+
// Contiguous deltas of the same block (same index and type) extend
|
|
5198
|
+
// the current entry; anything else opens a new one.
|
|
5199
|
+
if (message.event.type === "content_block_delta") {
|
|
5200
|
+
const delta = message.event.delta;
|
|
5201
|
+
const chunk = delta.type === "text_delta"
|
|
5202
|
+
? { type: "text", text: delta.text }
|
|
5203
|
+
: delta.type === "thinking_delta"
|
|
5204
|
+
? { type: "thinking", text: delta.thinking }
|
|
5205
|
+
: undefined;
|
|
5206
|
+
// Skip empty deltas (some gateways emit empty thinking chunks —
|
|
5207
|
+
// #793): appending "" is a no-op, but pushing a "" entry would
|
|
5208
|
+
// create a block the consolidated handler's `text.length > 0`
|
|
5209
|
+
// guard can never consume, stalling the diff cursor and
|
|
5210
|
+
// re-emitting the next block as a duplicate.
|
|
5211
|
+
if (chunk?.text) {
|
|
5212
|
+
const index = message.event.index;
|
|
5213
|
+
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
5214
|
+
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
5215
|
+
if (last && last.index === index && last.type === chunk.type) {
|
|
5216
|
+
last.text += chunk.text;
|
|
5217
|
+
}
|
|
5218
|
+
else {
|
|
5219
|
+
streamedBlocks.push({ index, type: chunk.type, text: chunk.text });
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
// The stream sees a tool_use first, so the tool's PostToolUse hook
|
|
5224
|
+
// is registered here, and publishes through this client: a
|
|
5225
|
+
// subagent's hook update must reach the child session too.
|
|
5226
|
+
for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
5019
5227
|
clientCapabilities: this.clientCapabilities,
|
|
5020
5228
|
toolCallCapabilities: this.toolCallCapabilities,
|
|
5021
5229
|
cwd: session.cwd,
|
|
@@ -5148,6 +5356,13 @@ export class ClaudeAcpAgent {
|
|
|
5148
5356
|
}
|
|
5149
5357
|
}
|
|
5150
5358
|
if (session.cancelled) {
|
|
5359
|
+
// The work the cancel stopped ends its tools with error results.
|
|
5360
|
+
// Until the client is told that the work ended, they close the
|
|
5361
|
+
// tool calls it opened. Everything else that work sends is
|
|
5362
|
+
// dropped, and so is everything after that report.
|
|
5363
|
+
if (message.type === "user" && !this.workEndReported(params.sessionId, session)) {
|
|
5364
|
+
await sendInterruptedToolResults(message);
|
|
5365
|
+
}
|
|
5151
5366
|
break;
|
|
5152
5367
|
}
|
|
5153
5368
|
// Synthetic assistant frames carry the CLI's local-command output.
|
|
@@ -5279,6 +5494,7 @@ export class ClaudeAcpAgent {
|
|
|
5279
5494
|
const streamed = streamedBlocksOf(message.parent_tool_use_id);
|
|
5280
5495
|
content = unstreamedRemainder(message.message.content, streamed);
|
|
5281
5496
|
streamed.length = 0;
|
|
5497
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
5282
5498
|
}
|
|
5283
5499
|
else if (message.type === "assistant") {
|
|
5284
5500
|
// Nested text/thinking stays internal for a client that does
|
|
@@ -5291,19 +5507,41 @@ export class ClaudeAcpAgent {
|
|
|
5291
5507
|
}
|
|
5292
5508
|
const acceptedPlanToolUseId = observeExitPlanToolResults(message, content, session);
|
|
5293
5509
|
let backgroundedToolCalls = new Set();
|
|
5510
|
+
let runningTerminal;
|
|
5294
5511
|
if (message.type === "user") {
|
|
5295
5512
|
rememberResolvedToolNames(session, content);
|
|
5296
5513
|
const backgroundBashTask = backgroundBashTaskFromToolResult(content, message.tool_use_result, session.toolUseCache);
|
|
5297
5514
|
if (backgroundBashTask)
|
|
5298
5515
|
await asyncTasks.taskBackgrounded(backgroundBashTask);
|
|
5516
|
+
// The command's terminal runs until its task ends, unless the
|
|
5517
|
+
// task has ended already (a fast command can end before the tool
|
|
5518
|
+
// result comes).
|
|
5519
|
+
const terminalId = backgroundBashTask?.toolCallId;
|
|
5520
|
+
if (terminalId &&
|
|
5521
|
+
this.toolCallCapabilities.terminalOutput &&
|
|
5522
|
+
!this.toolCallCapabilities.air.client &&
|
|
5523
|
+
session.liveBackgroundTasks.has(backgroundBashTask.taskId)) {
|
|
5524
|
+
runningTerminal = terminalId;
|
|
5525
|
+
(session.backgroundTerminals ??= new Map()).set(backgroundBashTask.taskId, {
|
|
5526
|
+
toolCallId: terminalId,
|
|
5527
|
+
...(message.parent_tool_use_id
|
|
5528
|
+
? { parentToolUseId: message.parent_tool_use_id }
|
|
5529
|
+
: {}),
|
|
5530
|
+
});
|
|
5531
|
+
}
|
|
5299
5532
|
await asyncTasks.toolResults(content);
|
|
5300
5533
|
backgroundedToolCalls = backgroundedBashToolCallIds(content, session.toolUseCache, asyncTasks, backgroundBashTask);
|
|
5301
5534
|
const resumedAgentId = resumedNativeSubagentId(message.tool_use_result);
|
|
5302
5535
|
if (resumedAgentId) {
|
|
5303
|
-
|
|
5304
|
-
await subagents.taskResumed(resumedAgentId, sendUpdate, sendMessageResumePrompt(session.toolUseCache, resumedAgentId, Array.isArray(content)
|
|
5536
|
+
const resume = sendMessageResume(session.toolUseCache, resumedAgentId, Array.isArray(content)
|
|
5305
5537
|
? content.flatMap((block) => block.type === "tool_result" ? [block.tool_use_id] : [])
|
|
5306
|
-
: [])
|
|
5538
|
+
: []);
|
|
5539
|
+
// In the RFD form the result can arrive after the run it
|
|
5540
|
+
// started ended, and then resumes nothing.
|
|
5541
|
+
if (!subagents.isEndedDelegation(resumedAgentId, resume?.toolUseId)) {
|
|
5542
|
+
resumeLiveTask(resumedAgentId);
|
|
5543
|
+
await subagents.sendMessageResumed(resumedAgentId, sendUpdate, resume?.text, resume?.toolUseId);
|
|
5544
|
+
}
|
|
5307
5545
|
}
|
|
5308
5546
|
}
|
|
5309
5547
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
@@ -5326,7 +5564,7 @@ export class ClaudeAcpAgent {
|
|
|
5326
5564
|
// filtered out of `content` above; blocks that do pass through
|
|
5327
5565
|
// (e.g. a subagent image) carry the stamped parentToolUseId
|
|
5328
5566
|
// meta and are excluded there.
|
|
5329
|
-
await sendUpdate(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled));
|
|
5567
|
+
await sendUpdate(withRunningTerminal(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled), runningTerminal));
|
|
5330
5568
|
}
|
|
5331
5569
|
break;
|
|
5332
5570
|
}
|
|
@@ -5350,10 +5588,14 @@ export class ClaudeAcpAgent {
|
|
|
5350
5588
|
if (toolCallId === null || !session.emittedToolCalls.has(toolCallId)) {
|
|
5351
5589
|
break;
|
|
5352
5590
|
}
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5591
|
+
// The beat is in the subagent whose Agent call is
|
|
5592
|
+
// `parent_tool_use_id`, unless it reports on that Agent call
|
|
5593
|
+
// itself: the Agent call's own heartbeat falls back to its id, and
|
|
5594
|
+
// a tool call is never its own parent.
|
|
5595
|
+
const subagentParentToolUseId = message.parent_tool_use_id &&
|
|
5596
|
+
message.parent_tool_use_id !== toolCallId &&
|
|
5597
|
+
[...session.liveBackgroundTasks.values()].some((task) => task.isSubagent && task.parentToolUseId === message.parent_tool_use_id)
|
|
5598
|
+
? message.parent_tool_use_id
|
|
5357
5599
|
: undefined;
|
|
5358
5600
|
// `tool_name` names the tool that reports the beat. When the beat
|
|
5359
5601
|
// falls back to the parent call, AIR gets the name of that call,
|
|
@@ -5576,17 +5818,29 @@ export class ClaudeAcpAgent {
|
|
|
5576
5818
|
// their compaction is running. Close it before ANY cancelled prompt
|
|
5577
5819
|
// resolves, not just the active-turn idle/backstop paths.
|
|
5578
5820
|
await session.contextCompaction?.interrupt();
|
|
5821
|
+
// A cancel drops the state of each tool call (the session that a
|
|
5822
|
+
// permission request emitted it in, the fields the client holds, the hook
|
|
5823
|
+
// callbacks), even when publishing fails. AIR's draft ends every child
|
|
5824
|
+
// here, so a later update of an ended child's tool call is then dropped
|
|
5825
|
+
// instead of reaching that child's session. The RFD form keeps the state:
|
|
5826
|
+
// a background subagent can keep running after a cancel
|
|
5827
|
+
// (agentclientprotocol/agent-client-protocol#2308), and its tool call that
|
|
5828
|
+
// a permission request emitted on the root before the child was announced
|
|
5829
|
+
// must stay on the root, which the RFD requires. Each tool call drops its
|
|
5830
|
+
// own state when it ends.
|
|
5579
5831
|
try {
|
|
5580
|
-
await session.nativeSubagentRuntime?.
|
|
5832
|
+
await session.nativeSubagentRuntime?.parentCancelled(session.nativeSubagentDeliver ??
|
|
5581
5833
|
(async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification))));
|
|
5582
5834
|
}
|
|
5583
5835
|
catch (error) {
|
|
5584
5836
|
this.logger.error(`Session ${params.sessionId}: failed to publish cancelled subagent state`, error);
|
|
5585
5837
|
}
|
|
5586
5838
|
finally {
|
|
5587
|
-
session.
|
|
5588
|
-
|
|
5589
|
-
|
|
5839
|
+
if (session.nativeSubagentRuntime?.form !== "rfd") {
|
|
5840
|
+
session.eagerToolCallSessions?.clear();
|
|
5841
|
+
session.toolCallFields?.clear();
|
|
5842
|
+
clearHookCallbacks(params.sessionId);
|
|
5843
|
+
}
|
|
5590
5844
|
}
|
|
5591
5845
|
// The user messages this cancel abandons while Claude Code may still have
|
|
5592
5846
|
// them queued: they are dropped from its queue before the interrupt.
|
|
@@ -5700,7 +5954,8 @@ export class ClaudeAcpAgent {
|
|
|
5700
5954
|
// permission request) is left to that cycle's interrupted result, like any
|
|
5701
5955
|
// live turn, or to the idle when no result comes. The captured outcome
|
|
5702
5956
|
// preserves usage and metadata (issue #844), while reporter state makes a
|
|
5703
|
-
// late checkpoint response harmless.
|
|
5957
|
+
// late checkpoint response harmless. The turn's open tool calls close
|
|
5958
|
+
// before its response, as in the consumer's settle.
|
|
5704
5959
|
// Since CLI 2.1.270, Claude Code reports `running` for as long as
|
|
5705
5960
|
// background agents live, so the session state cannot tell a live
|
|
5706
5961
|
// followup from a hold. With request markers, a followup is live when one
|
|
@@ -5713,7 +5968,10 @@ export class ClaudeAcpAgent {
|
|
|
5713
5968
|
if (active &&
|
|
5714
5969
|
(active.settling || (isHeldOpen(active) && !followupLive)) &&
|
|
5715
5970
|
(active.deferredSettle ?? active.settlingOutcome)) {
|
|
5971
|
+
const closingToolCalls = session.closeCancelledToolCalls?.(active);
|
|
5716
5972
|
session.fileChangeReporter?.finish(active.fileChangeReport, "cancelled");
|
|
5973
|
+
// Settled before the reports are awaited, so that the consumer, which
|
|
5974
|
+
// keeps running meanwhile, cannot settle the turn a second time.
|
|
5717
5975
|
active.settled = true;
|
|
5718
5976
|
// Mirror settleActive's invariants (it is consumer-scoped and
|
|
5719
5977
|
// unreachable from here): disarm the backstop — none should be
|
|
@@ -5745,7 +6003,15 @@ export class ClaudeAcpAgent {
|
|
|
5745
6003
|
if (session.lastSessionState !== "idle") {
|
|
5746
6004
|
session.owedTrailingIdles++;
|
|
5747
6005
|
}
|
|
5748
|
-
|
|
6006
|
+
try {
|
|
6007
|
+
await closingToolCalls;
|
|
6008
|
+
}
|
|
6009
|
+
catch (error) {
|
|
6010
|
+
this.logger.error(`Session ${params.sessionId}: failed to publish cancelled tool state`, error);
|
|
6011
|
+
}
|
|
6012
|
+
finally {
|
|
6013
|
+
active.resolve(cancelledOutcome(session, active));
|
|
6014
|
+
}
|
|
5749
6015
|
}
|
|
5750
6016
|
}
|
|
5751
6017
|
// A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
|
|
@@ -6048,7 +6314,12 @@ export class ClaudeAcpAgent {
|
|
|
6048
6314
|
// A replay rebuilds the client view, so its plan goes out again.
|
|
6049
6315
|
if (session?.taskState)
|
|
6050
6316
|
forgetPublishedTaskPlan(session.taskState);
|
|
6051
|
-
|
|
6317
|
+
// A subagent's text replays onto the root only for a client that gets it
|
|
6318
|
+
// there live: one without native subagent sessions that asked for the
|
|
6319
|
+
// flattened transcript. With native sessions the text goes to the child
|
|
6320
|
+
// session, which AIR's replay restores and the RFD form leaves out for now.
|
|
6321
|
+
const forwardSubagentText = !clientSupportsSubagents(this.clientCapabilities) &&
|
|
6322
|
+
(session?.forwardSubagentText ?? supportsSubagentTranscript(this.clientCapabilities));
|
|
6052
6323
|
const supportsTypedFailures = supportsAirSessionFailures(this.clientCapabilities);
|
|
6053
6324
|
const activeUsageLimit = supportsTypedFailures ? activeUsageLimitMessage(messages) : undefined;
|
|
6054
6325
|
const sessionFailures = session && supportsTypedFailures
|
|
@@ -6067,7 +6338,11 @@ export class ClaudeAcpAgent {
|
|
|
6067
6338
|
})
|
|
6068
6339
|
: undefined;
|
|
6069
6340
|
let replayTurnId;
|
|
6070
|
-
|
|
6341
|
+
// AIR's draft replays each child under a replay id. A client on the RFD
|
|
6342
|
+
// form gets each child under the session id it had live (see
|
|
6343
|
+
// `RfdSubagentReplay`).
|
|
6344
|
+
const form = subagentForm(this.clientCapabilities);
|
|
6345
|
+
const nativeReplayEnabled = form === "air";
|
|
6071
6346
|
const replayCompactionUpdates = clientSupportsCompactionUpdates(this.clientCapabilities);
|
|
6072
6347
|
const replayTerminalStates = new Map();
|
|
6073
6348
|
const replayChildren = new Map();
|
|
@@ -6170,8 +6445,17 @@ export class ClaudeAcpAgent {
|
|
|
6170
6445
|
// Only a history with an Agent or Task launch has subagent transcripts to read.
|
|
6171
6446
|
const subagents = nativeReplayEnabled && replayChildren.size > 0
|
|
6172
6447
|
? await subagentHistory(sessionId)
|
|
6173
|
-
: {
|
|
6448
|
+
: { agents: new Map() };
|
|
6174
6449
|
const replayedSubagents = new Set();
|
|
6450
|
+
const rfdHistory = form === "rfd" &&
|
|
6451
|
+
messages.some((message) => subagentLaunchIds(message.message?.content)
|
|
6452
|
+
.length > 0)
|
|
6453
|
+
? await subagentHistory(sessionId)
|
|
6454
|
+
: { agents: new Map() };
|
|
6455
|
+
const rfdReplay = form === "rfd"
|
|
6456
|
+
? new RfdSubagentReplay(rfdHistory, (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)))
|
|
6457
|
+
: undefined;
|
|
6458
|
+
rfdReplay?.scan(messages, sessionId);
|
|
6175
6459
|
// History persists no task_started frame, so a background task gets its
|
|
6176
6460
|
// spawn from the notification of its stop, with the tool call that
|
|
6177
6461
|
// started it. A task still running at the end of the history gets no
|
|
@@ -6190,6 +6474,8 @@ export class ClaudeAcpAgent {
|
|
|
6190
6474
|
});
|
|
6191
6475
|
// The replay counterpart of the live `task_notification` frame.
|
|
6192
6476
|
const restoreTaskNotification = async (notification) => {
|
|
6477
|
+
if (rfdReplay?.taskNotification(notification.task_id, notification.status))
|
|
6478
|
+
return;
|
|
6193
6479
|
const toolUseId = notification.tool_use_id;
|
|
6194
6480
|
if (!toolUseId)
|
|
6195
6481
|
return;
|
|
@@ -6216,7 +6502,8 @@ export class ClaudeAcpAgent {
|
|
|
6216
6502
|
});
|
|
6217
6503
|
await replayAsyncTasks.taskNotification(notification);
|
|
6218
6504
|
};
|
|
6219
|
-
|
|
6505
|
+
/** Replays `message`, from the transcript of `child` when it is set (the RFD form). */
|
|
6506
|
+
const replayMessage = async (message, child) => {
|
|
6220
6507
|
// A long history is sent in order, but with event-loop turns in between,
|
|
6221
6508
|
// so other sessions and incoming requests are not starved meanwhile.
|
|
6222
6509
|
const pause = messageLoopYielder.maybeYield();
|
|
@@ -6225,6 +6512,10 @@ export class ClaudeAcpAgent {
|
|
|
6225
6512
|
if (pending?.stopped || isReplayHiddenMetaMessage(message)) {
|
|
6226
6513
|
return;
|
|
6227
6514
|
}
|
|
6515
|
+
// In the RFD form a child's history comes from its own transcript, so a
|
|
6516
|
+
// subagent message that the session transcript stores inline is skipped.
|
|
6517
|
+
if (rfdReplay && !child && parentToolUseIdOf(message))
|
|
6518
|
+
return;
|
|
6228
6519
|
if (message.type === "user" &&
|
|
6229
6520
|
message.parent_tool_use_id === null &&
|
|
6230
6521
|
typeof message.uuid === "string" &&
|
|
@@ -6264,11 +6555,14 @@ export class ClaudeAcpAgent {
|
|
|
6264
6555
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
6265
6556
|
let content = message.message.content;
|
|
6266
6557
|
const parentToolUseId = parentToolUseIdOf(message);
|
|
6267
|
-
const replayTargetSessionId =
|
|
6268
|
-
?
|
|
6269
|
-
:
|
|
6558
|
+
const replayTargetSessionId = child
|
|
6559
|
+
? child.sessionId
|
|
6560
|
+
: nativeReplayEnabled && parentToolUseId
|
|
6561
|
+
? await announceReplayChild(parentToolUseId)
|
|
6562
|
+
: sessionId;
|
|
6270
6563
|
if (message.type === "assistant" &&
|
|
6271
6564
|
parentToolUseId &&
|
|
6565
|
+
!child &&
|
|
6272
6566
|
!nativeReplayEnabled &&
|
|
6273
6567
|
!forwardSubagentText) {
|
|
6274
6568
|
content = stripSubagentTextAndThinking(content);
|
|
@@ -6282,6 +6576,9 @@ export class ClaudeAcpAgent {
|
|
|
6282
6576
|
}
|
|
6283
6577
|
// Live, the prompt loop skips this record and the SDK frame reports the stop.
|
|
6284
6578
|
content = isTaskNotificationRecord(message) ? null : stripLocalCommandMetadata(content);
|
|
6579
|
+
if (content !== null && child && rfdReplay) {
|
|
6580
|
+
content = await rfdReplay.childContent(child, message, content);
|
|
6581
|
+
}
|
|
6285
6582
|
if (content === null)
|
|
6286
6583
|
return;
|
|
6287
6584
|
}
|
|
@@ -6353,32 +6650,37 @@ export class ClaudeAcpAgent {
|
|
|
6353
6650
|
if (nativeReplayEnabled && message.type === "assistant") {
|
|
6354
6651
|
for (const toolUseId of subagentLaunchIds(content)) {
|
|
6355
6652
|
await announceReplayChild(toolUseId);
|
|
6356
|
-
const agentId = subagents.
|
|
6653
|
+
const agentId = subagents.agents.get(toolUseId)?.agentId;
|
|
6357
6654
|
if (!agentId || replayedSubagents.has(agentId))
|
|
6358
6655
|
continue;
|
|
6359
6656
|
replayedSubagents.add(agentId);
|
|
6360
|
-
|
|
6361
|
-
try {
|
|
6362
|
-
// With the project directory, the SDK skips its search of every project.
|
|
6363
|
-
// A session opened from another directory falls back to that search.
|
|
6364
|
-
if (subagents.dir) {
|
|
6365
|
-
childMessages = await getSubagentMessages(sessionId, agentId, { dir: subagents.dir });
|
|
6366
|
-
}
|
|
6367
|
-
if (childMessages.length === 0) {
|
|
6368
|
-
childMessages = await getSubagentMessages(sessionId, agentId);
|
|
6369
|
-
}
|
|
6370
|
-
}
|
|
6371
|
-
catch (error) {
|
|
6372
|
-
this.logger.error(`Failed to read the history of subagent ${agentId}:`, error);
|
|
6373
|
-
}
|
|
6657
|
+
const childMessages = await readSubagentMessages(sessionId, agentId, subagents, this.logger);
|
|
6374
6658
|
registerLaunches(childMessages);
|
|
6375
6659
|
for (const childMessage of childMessages)
|
|
6376
6660
|
await replayMessage(childMessage);
|
|
6377
6661
|
}
|
|
6378
6662
|
}
|
|
6663
|
+
// The RFD form announces a child right after its launch, then sends its
|
|
6664
|
+
// own transcript to it.
|
|
6665
|
+
if (rfdReplay && message.type === "assistant") {
|
|
6666
|
+
for (const toolUseId of subagentLaunchIds(content)) {
|
|
6667
|
+
const launched = rfdReplay.childOfLaunch(toolUseId);
|
|
6668
|
+
if (!launched || launched.announced)
|
|
6669
|
+
continue;
|
|
6670
|
+
await rfdReplay.announce(launched);
|
|
6671
|
+
const childMessages = await readSubagentMessages(sessionId, launched.sessionId, rfdHistory, this.logger);
|
|
6672
|
+
rfdReplay.scan(childMessages, launched.sessionId);
|
|
6673
|
+
for (const childMessage of childMessages)
|
|
6674
|
+
await replayMessage(childMessage, launched);
|
|
6675
|
+
await rfdReplay.childReplayed(launched, childMessages);
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6379
6678
|
};
|
|
6380
6679
|
for (const message of messages)
|
|
6381
6680
|
await replayMessage(message);
|
|
6681
|
+
if (rfdReplay && !pending?.stopped) {
|
|
6682
|
+
await rfdReplay.finish(clientSupportsNotices(this.clientCapabilities), sessionId);
|
|
6683
|
+
}
|
|
6382
6684
|
if (nativeReplayEnabled && !pending?.stopped) {
|
|
6383
6685
|
// Claude history persists sidechain messages and Agent/Task tool uses,
|
|
6384
6686
|
// but not task_started/task_updated lifecycle frames. Recover terminal
|
|
@@ -6413,6 +6715,16 @@ export class ClaudeAcpAgent {
|
|
|
6413
6715
|
const response = await this.client.writeTextFile(params);
|
|
6414
6716
|
return response;
|
|
6415
6717
|
}
|
|
6718
|
+
/** Runs `request`, a request that `requestSessionId` sends for the session
|
|
6719
|
+
* `ownerSessionId`. When that is a subagent's child session, the child
|
|
6720
|
+
* reports that it awaits the user while the request is open (the RFD form,
|
|
6721
|
+
* see `NativeSubagentRuntime.awaitingUser`). */
|
|
6722
|
+
withChildAwaitingUser(ownerSessionId, requestSessionId, request) {
|
|
6723
|
+
const runtime = this.sessions[ownerSessionId]?.nativeSubagentRuntime;
|
|
6724
|
+
return runtime && requestSessionId !== ownerSessionId
|
|
6725
|
+
? runtime.awaitingUser(requestSessionId, request)
|
|
6726
|
+
: request();
|
|
6727
|
+
}
|
|
6416
6728
|
/** Mark a client request of the subagent `agentID` (absent for the main
|
|
6417
6729
|
* conversation) as awaiting user input for exactly the lifetime of its
|
|
6418
6730
|
* promise (see `Session.openUserInputs`). Steering consults the open
|
|
@@ -6507,6 +6819,13 @@ export class ClaudeAcpAgent {
|
|
|
6507
6819
|
return;
|
|
6508
6820
|
this.startForeground(sessionId, AUTONOMOUS_CYCLE);
|
|
6509
6821
|
}
|
|
6822
|
+
/** Whether the client has been told that the session's work ended: on v2 by
|
|
6823
|
+
* an idle state, so no foreground work is open, including a cycle that
|
|
6824
|
+
* Claude Code started on its own; on v1 by the response to the turn's
|
|
6825
|
+
* prompt, so no turn is active. */
|
|
6826
|
+
workEndReported(sessionId, session) {
|
|
6827
|
+
return this.foregroundEvents ? !this.foregroundWork.has(sessionId) : !session.activeTurn;
|
|
6828
|
+
}
|
|
6510
6829
|
/** End the session's model cycle that Claude Code started on its own, if one
|
|
6511
6830
|
* runs. */
|
|
6512
6831
|
endAutonomousCycle(sessionId, end) {
|
|
@@ -6536,7 +6855,7 @@ export class ClaudeAcpAgent {
|
|
|
6536
6855
|
// The request goes to a subagent's child session for a tool call of that
|
|
6537
6856
|
// subagent, but it blocks the turn of the owner session.
|
|
6538
6857
|
try {
|
|
6539
|
-
return await this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID);
|
|
6858
|
+
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, () => this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID));
|
|
6540
6859
|
}
|
|
6541
6860
|
catch (error) {
|
|
6542
6861
|
if (signal.aborted) {
|
|
@@ -6576,6 +6895,8 @@ export class ClaudeAcpAgent {
|
|
|
6576
6895
|
return;
|
|
6577
6896
|
}
|
|
6578
6897
|
session.emittedToolCalls.add(toolCallId);
|
|
6898
|
+
if (parentToolUseId)
|
|
6899
|
+
recordToolCallParent(session, toolCallId, parentToolUseId);
|
|
6579
6900
|
// SandboxNetworkAccess is a permission-only callback, not an executing
|
|
6580
6901
|
// tool: the SDK does not send a tool_result for its synthetic id.
|
|
6581
6902
|
if (!parentToolUseId &&
|
|
@@ -6600,6 +6921,7 @@ export class ClaudeAcpAgent {
|
|
|
6600
6921
|
session.emittedToolCalls.delete(toolCallId);
|
|
6601
6922
|
session.eagerToolCallSessions?.delete(toolCallId);
|
|
6602
6923
|
session.toolCallFields?.delete(toolCallId);
|
|
6924
|
+
session.toolCallParents?.delete(toolCallId);
|
|
6603
6925
|
throw error;
|
|
6604
6926
|
}
|
|
6605
6927
|
}
|
|
@@ -6874,7 +7196,7 @@ export class ClaudeAcpAgent {
|
|
|
6874
7196
|
const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
6875
7197
|
let response;
|
|
6876
7198
|
try {
|
|
6877
|
-
response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal), agentID);
|
|
7199
|
+
response = await this.withChildAwaitingUser(sessionId, requestSessionId, () => this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal), agentID));
|
|
6878
7200
|
}
|
|
6879
7201
|
catch (error) {
|
|
6880
7202
|
// A cancellation we requested (signal aborted) settles as an aborted tool
|
|
@@ -7745,6 +8067,8 @@ export class ClaudeAcpAgent {
|
|
|
7745
8067
|
prompt: input,
|
|
7746
8068
|
options,
|
|
7747
8069
|
});
|
|
8070
|
+
this.useAuthStatusSettings(settingsManager, configuredSettingsObject, userProvidedOptions?.env);
|
|
8071
|
+
const authSettingsEpoch = this.authEpoch;
|
|
7748
8072
|
timing.phase("prepare-query");
|
|
7749
8073
|
// `query()` spawns the CLI at once. Any throw between here and the
|
|
7750
8074
|
// registration in `this.sessions` would leave that child process running
|
|
@@ -7767,7 +8091,9 @@ export class ClaudeAcpAgent {
|
|
|
7767
8091
|
// Publish the identity BEFORE the guard can refuse this session. A
|
|
7768
8092
|
// refusal is exactly when the client most needs to know which account it
|
|
7769
8093
|
// was refused for.
|
|
7770
|
-
this.
|
|
8094
|
+
if (authSettingsEpoch === this.authEpoch) {
|
|
8095
|
+
this.publishSessionAccountIdentity(initializationResult.account, settingsManager.getApiKeyHelper(configuredSettingsObject));
|
|
8096
|
+
}
|
|
7771
8097
|
// Shared with the per-turn guard, so "warn once" spans the whole session.
|
|
7772
8098
|
const claudeSubscriptionGuard = {};
|
|
7773
8099
|
if (this.claudeSubscriptionGuardActive()) {
|
|
@@ -7969,7 +8295,7 @@ export class ClaudeAcpAgent {
|
|
|
7969
8295
|
messageIdToUuid: creationOpts.replayState?.messageIdToUuid ?? new Map(),
|
|
7970
8296
|
sessionFailureState: creationOpts.replayState?.sessionFailureState ?? createSessionFailureState(),
|
|
7971
8297
|
claudeSubscriptionGuard,
|
|
7972
|
-
accountKind: fromAccountInfo(initializationResult.account)?.kind,
|
|
8298
|
+
accountKind: fromAccountInfo(initializationResult.account, settingsManager.getApiKeyHelper(configuredSettingsObject))?.kind,
|
|
7973
8299
|
fileChangeReporter,
|
|
7974
8300
|
};
|
|
7975
8301
|
timing.phase("register");
|
|
@@ -8121,27 +8447,34 @@ function cancelledOutcome(session, turn) {
|
|
|
8121
8447
|
}
|
|
8122
8448
|
/** Whether the model request that `status` (a `status: requesting` frame)
|
|
8123
8449
|
* starts may belong to the work a cancel stopped. It may while:
|
|
8124
|
-
* - a turn is unsettled: the stopped
|
|
8125
|
-
*
|
|
8126
|
-
* cancel awaited is recorded only when the cancel sweeps the queue) or
|
|
8127
|
-
* whose echo ends the cancel right after its request anyway;
|
|
8450
|
+
* - a turn in {@link Session.cancelledPrompts} is unsettled: the stopped
|
|
8451
|
+
* turn, whose result is still to come;
|
|
8128
8452
|
* - the orphan accounting still expects the result of an abandoned prompt
|
|
8129
8453
|
* that Claude Code may still run, which it can fold into its own
|
|
8130
8454
|
* follow-up after that follow-up's first request;
|
|
8131
8455
|
* - the request serves one of {@link Session.cancelledPrompts}, as an
|
|
8132
8456
|
* abandoned prompt's request does even when the interrupt receipt left it
|
|
8133
|
-
* out of the orphan accounting
|
|
8457
|
+
* out of the orphan accounting;
|
|
8458
|
+
* - the request serves an unsettled prompt. One that arrived while the cancel
|
|
8459
|
+
* awaited is recorded only when the cancel sweeps the queue, and its echo
|
|
8460
|
+
* ends the cancel right after its request when the cancel does not.
|
|
8461
|
+
* A prompt that is merely queued does not make another request belong to
|
|
8462
|
+
* the stopped work: a follow-up that Claude Code runs before a prompt sent
|
|
8463
|
+
* after the cancel names no prompt. */
|
|
8134
8464
|
function servesCancelledWork(session, status) {
|
|
8135
8465
|
const cancelled = session.cancelledPrompts;
|
|
8136
8466
|
if (!cancelled)
|
|
8137
8467
|
return false;
|
|
8138
|
-
|
|
8468
|
+
const unsettled = (session.turnQueue ?? []).filter((turn) => !turn.settled);
|
|
8469
|
+
if (unsettled.some((turn) => cancelled.has(turn.promptUuid)))
|
|
8139
8470
|
return true;
|
|
8140
8471
|
if ((session.pendingOrphanResults ?? 0) > 0 || (session.orphanCommands?.size ?? 0) > 0) {
|
|
8141
8472
|
return true;
|
|
8142
8473
|
}
|
|
8143
8474
|
const served = status.user_message_uuids;
|
|
8144
|
-
return (Array.isArray(served) &&
|
|
8475
|
+
return (Array.isArray(served) &&
|
|
8476
|
+
served.some((uuid) => typeof uuid === "string" &&
|
|
8477
|
+
(cancelled.has(uuid) || unsettled.some((turn) => turn.promptUuid === uuid))));
|
|
8145
8478
|
}
|
|
8146
8479
|
/** `_meta.quota` for a prompt response: what the turn spent, shaped like
|
|
8147
8480
|
* codex-acp's (snake_case container keys, camelCase counters) so a client
|
|
@@ -8176,15 +8509,20 @@ function quotaMeta(usage, modelUsage) {
|
|
|
8176
8509
|
* foreground of the main conversation, since the tool call that spawned it
|
|
8177
8510
|
* waits on it, and so on up a chain of such subagents. A request of a
|
|
8178
8511
|
* subagent in the background, or nested inside one, does not: the main
|
|
8179
|
-
* conversation goes on without it.
|
|
8180
|
-
*
|
|
8512
|
+
* conversation goes on without it. Nor does one of an agent without a
|
|
8513
|
+
* record: a teammate, a settled subagent, or one that Claude Code left out
|
|
8514
|
+
* of its background tasks and the consumer pruned, which only happens to a
|
|
8515
|
+
* subagent nested in a background one. A subagent that the main
|
|
8516
|
+
* conversation waits on has a record by the time it asks, as its
|
|
8517
|
+
* `task_started` comes first; when the consumer reads the request first, the
|
|
8518
|
+
* `task_started` checks again. */
|
|
8181
8519
|
function blocksForeground(session, agentID) {
|
|
8182
8520
|
const seen = new Set();
|
|
8183
8521
|
for (let id = agentID; id !== undefined && !seen.has(id);) {
|
|
8184
8522
|
seen.add(id);
|
|
8185
8523
|
const task = session.liveBackgroundTasks.get(id);
|
|
8186
8524
|
if (!task)
|
|
8187
|
-
return
|
|
8525
|
+
return false;
|
|
8188
8526
|
if (task.backgrounded)
|
|
8189
8527
|
return false;
|
|
8190
8528
|
id = task.parentTaskId;
|
|
@@ -8862,6 +9200,18 @@ function isTaskTool(toolName) {
|
|
|
8862
9200
|
function shouldEmitToolCall(toolName) {
|
|
8863
9201
|
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
|
8864
9202
|
}
|
|
9203
|
+
const MAX_TOOL_CALL_PARENTS = 1000;
|
|
9204
|
+
/** Records the Agent/Task call that started the subagent making `toolCallId`. */
|
|
9205
|
+
function recordToolCallParent(session, toolCallId, parentToolUseId) {
|
|
9206
|
+
const parents = (session.toolCallParents ??= new Map());
|
|
9207
|
+
parents.delete(toolCallId);
|
|
9208
|
+
parents.set(toolCallId, parentToolUseId);
|
|
9209
|
+
if (parents.size > MAX_TOOL_CALL_PARENTS) {
|
|
9210
|
+
const oldest = parents.keys().next().value;
|
|
9211
|
+
if (oldest !== undefined)
|
|
9212
|
+
parents.delete(oldest);
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
8865
9215
|
const MAX_DISPATCHED_TOOL_CALLS = 1000;
|
|
8866
9216
|
/** Records a tool call that Claude Code sent to the tool runner. */
|
|
8867
9217
|
function recordDispatchedToolCall(session, toolCallId) {
|
|
@@ -8974,6 +9324,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8974
9324
|
let update = null;
|
|
8975
9325
|
// The tool call that this chunk finishes, if it is a tool result.
|
|
8976
9326
|
let finishedToolCallId;
|
|
9327
|
+
// The plan that this chunk approves, if it is an ExitPlanMode result.
|
|
9328
|
+
let approvedPlanUpdate;
|
|
8977
9329
|
switch (chunk.type) {
|
|
8978
9330
|
case "text":
|
|
8979
9331
|
case "text_delta": {
|
|
@@ -9131,7 +9483,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9131
9483
|
update: {
|
|
9132
9484
|
toolCallId: chunk.tool_use_id,
|
|
9133
9485
|
sessionUpdate: "tool_call_update",
|
|
9134
|
-
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution),
|
|
9486
|
+
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution, { cancelled: options?.cancelled }),
|
|
9135
9487
|
rawOutput: chunk.content,
|
|
9136
9488
|
},
|
|
9137
9489
|
});
|
|
@@ -9160,7 +9512,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9160
9512
|
},
|
|
9161
9513
|
toolCallId: chunk.tool_use_id,
|
|
9162
9514
|
sessionUpdate: "tool_call_update",
|
|
9163
|
-
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution),
|
|
9515
|
+
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution, { cancelled: options?.cancelled }),
|
|
9164
9516
|
rawOutput: chunk.content,
|
|
9165
9517
|
},
|
|
9166
9518
|
});
|
|
@@ -9212,6 +9564,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9212
9564
|
.result(toolUse, chunk, {
|
|
9213
9565
|
structured: toolUseResult,
|
|
9214
9566
|
nonExecution,
|
|
9567
|
+
cancelled: options?.cancelled,
|
|
9215
9568
|
})
|
|
9216
9569
|
.reverse();
|
|
9217
9570
|
for (const outputUpdate of rest.reverse()) {
|
|
@@ -9220,6 +9573,19 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9220
9573
|
}
|
|
9221
9574
|
output.push({ sessionId, update: outputUpdate });
|
|
9222
9575
|
}
|
|
9576
|
+
// An approved plan of the main conversation becomes the session's
|
|
9577
|
+
// markdown plan, after the tool call's result. An approval that
|
|
9578
|
+
// clears the context ends the tool with an error result instead, and
|
|
9579
|
+
// the restart into the fresh context publishes that plan (see
|
|
9580
|
+
// ExitPlanCoordinator.restart).
|
|
9581
|
+
if (toolUse.name === "ExitPlanMode" &&
|
|
9582
|
+
options?.clientCapabilities?.plan &&
|
|
9583
|
+
!options.parentToolUseId &&
|
|
9584
|
+
!("is_error" in chunk && chunk.is_error)) {
|
|
9585
|
+
const plan = approvedPlan(toolUse.input, toolUseResult);
|
|
9586
|
+
if (plan)
|
|
9587
|
+
approvedPlanUpdate = planModeUpdate(plan);
|
|
9588
|
+
}
|
|
9223
9589
|
update = finalUpdate;
|
|
9224
9590
|
}
|
|
9225
9591
|
// The tool_use is fully resolved now — drop it so a long session doesn't
|
|
@@ -9259,6 +9625,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9259
9625
|
output.push({ sessionId, update });
|
|
9260
9626
|
}
|
|
9261
9627
|
}
|
|
9628
|
+
if (approvedPlanUpdate)
|
|
9629
|
+
output.push({ sessionId, update: approvedPlanUpdate });
|
|
9262
9630
|
if (finishedToolCallId !== undefined) {
|
|
9263
9631
|
// The PostToolUse hook can still send the final diff, so the fields
|
|
9264
9632
|
// stay tracked until its callback leaves the registry.
|