@agentclientprotocol/claude-agent-acp 0.89.2-preview.9 → 0.90.1-preview.1
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 +56 -8
- package/dist/acp-agent.d.ts +50 -1
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +493 -147
- 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 +228 -11
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +489 -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 +44 -2
- package/dist/subagent-history.d.ts.map +1 -1
- package/dist/subagent-history.js +116 -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, subagentLaunchIds, subagentLineage, } 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";
|
|
@@ -536,40 +537,6 @@ function parentToolUseIdOf(message) {
|
|
|
536
537
|
return null;
|
|
537
538
|
return typeof message.parent_tool_use_id === "string" ? message.parent_tool_use_id : null;
|
|
538
539
|
}
|
|
539
|
-
/** The ids of the Agent and Task tool uses in the content of a message. */
|
|
540
|
-
function subagentLaunchIds(content) {
|
|
541
|
-
if (!Array.isArray(content))
|
|
542
|
-
return [];
|
|
543
|
-
return content.flatMap((block) => typeof block === "object" &&
|
|
544
|
-
block !== null &&
|
|
545
|
-
block.type === "tool_use" &&
|
|
546
|
-
typeof block.id === "string" &&
|
|
547
|
-
isNativeSubagentControlTool(block.name)
|
|
548
|
-
? [block.id]
|
|
549
|
-
: []);
|
|
550
|
-
}
|
|
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
540
|
function stripSubagentTextAndThinking(content) {
|
|
574
541
|
if (!Array.isArray(content))
|
|
575
542
|
return content;
|
|
@@ -1172,6 +1139,8 @@ export class ClaudeAcpAgent {
|
|
|
1172
1139
|
/** Last auth identity reported to the client, connection-scoped like
|
|
1173
1140
|
* `authenticate`/`logout`. Undefined means "not determined yet". */
|
|
1174
1141
|
currentAuthStatus;
|
|
1142
|
+
authStatusSettings;
|
|
1143
|
+
authStatusHelper;
|
|
1175
1144
|
/** In-flight `claude auth status --json` probe, shared by every caller so
|
|
1176
1145
|
* a concurrent `initialize` and start-of-prompt read never spawn two CLI
|
|
1177
1146
|
* processes. */
|
|
@@ -1232,6 +1201,7 @@ export class ClaudeAcpAgent {
|
|
|
1232
1201
|
},
|
|
1233
1202
|
applyFastMode: (session, enabled) => this.applyFastMode(session, enabled),
|
|
1234
1203
|
sessionUpdate: (notification) => this.client.sessionUpdate(notification),
|
|
1204
|
+
supportsPlanOperations: () => this.clientCapabilities?.plan != null,
|
|
1235
1205
|
ensureConsumer: (session, id) => this.ensureConsumer(session, id),
|
|
1236
1206
|
logError: (message, error) => this.logger.error(message, error),
|
|
1237
1207
|
destroyReplacement: (id, session) => {
|
|
@@ -1378,6 +1348,8 @@ export class ClaudeAcpAgent {
|
|
|
1378
1348
|
terminalAuthMethods.push(consoleLoginMethod);
|
|
1379
1349
|
}
|
|
1380
1350
|
}
|
|
1351
|
+
// The subagents RFD has only the client capability. AIR's draft also had
|
|
1352
|
+
// this agent capability, which AIR gets (`docs/air-extensions.md`).
|
|
1381
1353
|
const sessionCapabilities = {
|
|
1382
1354
|
additionalDirectories: {},
|
|
1383
1355
|
close: {},
|
|
@@ -1385,7 +1357,7 @@ export class ClaudeAcpAgent {
|
|
|
1385
1357
|
fork: {},
|
|
1386
1358
|
list: {},
|
|
1387
1359
|
resume: {},
|
|
1388
|
-
subagents: {},
|
|
1360
|
+
...(this.toolCallCapabilities.air.client ? { subagents: {} } : {}),
|
|
1389
1361
|
};
|
|
1390
1362
|
return {
|
|
1391
1363
|
protocolVersion: 1,
|
|
@@ -1506,14 +1478,30 @@ export class ClaudeAcpAgent {
|
|
|
1506
1478
|
configOptions: response.configOptions,
|
|
1507
1479
|
};
|
|
1508
1480
|
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Copies the session's transcript into a new session, then starts that
|
|
1483
|
+
* session, so the client can prompt it as soon as the response arrives, as
|
|
1484
|
+
* the schema requires of a fork. It starts like a resumed session: from the
|
|
1485
|
+
* copied transcript, with the model and the permission mode it records. A
|
|
1486
|
+
* later load or resume with the same settings reuses it.
|
|
1487
|
+
*/
|
|
1509
1488
|
async unstable_forkSession(params) {
|
|
1510
1489
|
if (this.providerUpdate)
|
|
1511
1490
|
await this.providerUpdate;
|
|
1512
|
-
|
|
1491
|
+
const { sessionId } = await forkSession(params, {
|
|
1513
1492
|
liveMessageIdToUuid: this.sessions[params.sessionId]?.messageIdToUuid,
|
|
1514
1493
|
logger: this.logger,
|
|
1515
1494
|
messageIdForGrouping,
|
|
1516
1495
|
});
|
|
1496
|
+
const { modes, configOptions } = await this.getOrCreateSession({
|
|
1497
|
+
sessionId,
|
|
1498
|
+
cwd: params.cwd,
|
|
1499
|
+
mcpServers: params.mcpServers,
|
|
1500
|
+
additionalDirectories: params.additionalDirectories,
|
|
1501
|
+
_meta: params._meta,
|
|
1502
|
+
});
|
|
1503
|
+
this.afterSetupResponse(sessionId, params.mcpServers ?? []);
|
|
1504
|
+
return { sessionId, modes, configOptions };
|
|
1517
1505
|
}
|
|
1518
1506
|
async resumeSession(params) {
|
|
1519
1507
|
if (this.providerUpdate)
|
|
@@ -1682,7 +1670,22 @@ export class ClaudeAcpAgent {
|
|
|
1682
1670
|
* under an apiKeyHelper) means "nothing to add", not "logged out" — keep
|
|
1683
1671
|
* what the CLI probe already established instead of overwriting it.
|
|
1684
1672
|
*/
|
|
1685
|
-
|
|
1673
|
+
useAuthStatusSettings(manager, settingsOverride, launchEnv) {
|
|
1674
|
+
const helper = manager.getApiKeyHelper(settingsOverride);
|
|
1675
|
+
const changed = manager.getCwd() !== (this.authStatusSettings?.manager.getCwd() ?? process.cwd()) ||
|
|
1676
|
+
helper !== this.authStatusHelper ||
|
|
1677
|
+
JSON.stringify(launchEnv) !== JSON.stringify(this.authStatusSettings?.env);
|
|
1678
|
+
this.authStatusSettings = { manager, override: settingsOverride, env: launchEnv };
|
|
1679
|
+
this.authStatusHelper = helper;
|
|
1680
|
+
if (changed) {
|
|
1681
|
+
// The epoch bump discards any probe that read the old context, and the
|
|
1682
|
+
// session account may carry no identity signal (an apiKeyHelper), so
|
|
1683
|
+
// only a probe in the new context can classify it.
|
|
1684
|
+
this.authEpoch += 1;
|
|
1685
|
+
void this.probeCliAuthStatus({ fresh: true });
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
publishSessionAccountIdentity(account, apiKeyHelper) {
|
|
1686
1689
|
if (this.providerConfig) {
|
|
1687
1690
|
if (!this.loggedOverriddenAccount) {
|
|
1688
1691
|
this.loggedOverriddenAccount = true;
|
|
@@ -1693,7 +1696,7 @@ export class ClaudeAcpAgent {
|
|
|
1693
1696
|
if (this.gatewayAuthRequest) {
|
|
1694
1697
|
return;
|
|
1695
1698
|
}
|
|
1696
|
-
const fromSession = fromAccountInfo(account);
|
|
1699
|
+
const fromSession = fromAccountInfo(account, apiKeyHelper);
|
|
1697
1700
|
if (fromSession) {
|
|
1698
1701
|
this.setAuthStatus(fromSession);
|
|
1699
1702
|
}
|
|
@@ -1726,6 +1729,8 @@ export class ClaudeAcpAgent {
|
|
|
1726
1729
|
// auth-affecting event describes a world that no longer exists. Remember
|
|
1727
1730
|
// which one this read belongs to and drop the answer if it moved on.
|
|
1728
1731
|
const epoch = this.authEpoch;
|
|
1732
|
+
const context = this.authStatusSettings;
|
|
1733
|
+
const apiKeyHelper = context?.manager.getApiKeyHelper(context.override);
|
|
1729
1734
|
// ACP gateway auth bypasses the CLI credential store entirely, so the
|
|
1730
1735
|
// probe would report an identity that is not the one being used. A mere
|
|
1731
1736
|
// client provider override (`providers/set`) does NOT skip the probe: it
|
|
@@ -1737,8 +1742,13 @@ export class ClaudeAcpAgent {
|
|
|
1737
1742
|
let stdout;
|
|
1738
1743
|
try {
|
|
1739
1744
|
const cliPath = await claudeCliPath();
|
|
1740
|
-
|
|
1745
|
+
const args = ["auth", "status", "--json"];
|
|
1746
|
+
if (context?.override)
|
|
1747
|
+
args.push("--settings", JSON.stringify(context.override));
|
|
1748
|
+
({ stdout } = await execFileAsync(cliPath, args, {
|
|
1741
1749
|
timeout: AUTH_STATUS_PROBE_TIMEOUT_MS,
|
|
1750
|
+
cwd: context?.manager.getCwd() ?? process.cwd(),
|
|
1751
|
+
env: context?.env ? { ...process.env, ...context.env } : process.env,
|
|
1742
1752
|
}));
|
|
1743
1753
|
}
|
|
1744
1754
|
catch (error) {
|
|
@@ -1761,7 +1771,16 @@ export class ClaudeAcpAgent {
|
|
|
1761
1771
|
}
|
|
1762
1772
|
stdout = failed.stdout;
|
|
1763
1773
|
}
|
|
1764
|
-
|
|
1774
|
+
let status = fromCliStatus(stdout, apiKeyHelper);
|
|
1775
|
+
if (!context && status?.kind === "api_key" && status.detail === "API key helper") {
|
|
1776
|
+
try {
|
|
1777
|
+
const resolved = await resolveSettings({ cwd: process.cwd() });
|
|
1778
|
+
status = fromCliStatus(stdout, filterEscalatingDefaultMode(resolved).apiKeyHelper);
|
|
1779
|
+
}
|
|
1780
|
+
catch {
|
|
1781
|
+
this.logger.error("Could not resolve auth source settings");
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1765
1784
|
if (!status) {
|
|
1766
1785
|
this.logger.error("claude auth status returned unparseable output");
|
|
1767
1786
|
return undefined;
|
|
@@ -2027,6 +2046,7 @@ export class ClaudeAcpAgent {
|
|
|
2027
2046
|
// that finds the new credential must not be the one the refusal skipped.
|
|
2028
2047
|
// The guard itself consumes the result only at the next turn boundary (see
|
|
2029
2048
|
// `markSessionsWhoseAccountKindChanged`).
|
|
2049
|
+
this.useAuthStatusSettings(session.settingsManager, session.effortSettingsOverride, session.creationParams?._meta?.claudeCode?.options?.env);
|
|
2030
2050
|
void this.probeCliAuthStatus();
|
|
2031
2051
|
const signOutRespawn = this.respawnSignedOutSession(params.sessionId, session);
|
|
2032
2052
|
if (signOutRespawn)
|
|
@@ -2245,7 +2265,7 @@ export class ClaudeAcpAgent {
|
|
|
2245
2265
|
logger: this.logger,
|
|
2246
2266
|
// The guard reads the account anyway; reuse that read to keep the
|
|
2247
2267
|
// reported identity current, refusal or not.
|
|
2248
|
-
onAccount: (account) => this.publishSessionAccountIdentity(account),
|
|
2268
|
+
onAccount: (account) => this.publishSessionAccountIdentity(account, session.settingsManager.getApiKeyHelper(session.effortSettingsOverride)),
|
|
2249
2269
|
});
|
|
2250
2270
|
}
|
|
2251
2271
|
async goal(params) {
|
|
@@ -2500,6 +2520,10 @@ export class ClaudeAcpAgent {
|
|
|
2500
2520
|
streamedBlocksByParent.set(key, (blocks = []));
|
|
2501
2521
|
return blocks;
|
|
2502
2522
|
};
|
|
2523
|
+
// The streams, keyed like streamedBlocksByParent, that dropped a delta
|
|
2524
|
+
// after a cancel since their record last reset, so the record no longer
|
|
2525
|
+
// matches what the client got (see the stream_event case).
|
|
2526
|
+
const droppedStreams = new Set();
|
|
2503
2527
|
// A client gets the consolidated subagent text when it negotiated the
|
|
2504
2528
|
// transcript extension or the `forwardSubagentText` session option. AIR
|
|
2505
2529
|
// also gets it with native subagent sessions, and AIR gets no streamed
|
|
@@ -2528,7 +2552,7 @@ export class ClaudeAcpAgent {
|
|
|
2528
2552
|
* recognizable by the `parentToolUseId` meta that toAcpNotifications
|
|
2529
2553
|
* stamps from `parent_tool_use_id`, and never reach the top-level feed
|
|
2530
2554
|
* as the turn's answer. */
|
|
2531
|
-
const subagents = (session.nativeSubagentRuntime ??= new NativeSubagentRuntime(
|
|
2555
|
+
const subagents = (session.nativeSubagentRuntime ??= new NativeSubagentRuntime(subagentForm(this.clientCapabilities), params.sessionId, session, async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), this.logger, (agentId) => subagentLineage(params.sessionId, agentId, this.logger)));
|
|
2532
2556
|
// Adapter-composed advisories (hook feedback, model fallbacks) are live
|
|
2533
2557
|
// events, not something the model said. Clients on the notice contract get
|
|
2534
2558
|
// them as `notice` updates; the rest keep the bold-label transcript line.
|
|
@@ -2563,6 +2587,19 @@ export class ClaudeAcpAgent {
|
|
|
2563
2587
|
const eagerOwnerSessionId = toolCallId
|
|
2564
2588
|
? session.eagerToolCallSessions?.get(toolCallId)
|
|
2565
2589
|
: undefined;
|
|
2590
|
+
// Before routing, which can buffer or drop the update: a cancel still has
|
|
2591
|
+
// to know the parent of a call that is open here. A later update can be
|
|
2592
|
+
// the first to name it, for a call that a permission request emitted.
|
|
2593
|
+
if (toolCallId &&
|
|
2594
|
+
update.sessionUpdate === "tool_call_update" &&
|
|
2595
|
+
isFinalToolCallStatus(update.status)) {
|
|
2596
|
+
session.toolCallParents?.delete(toolCallId);
|
|
2597
|
+
}
|
|
2598
|
+
else if (toolCallId &&
|
|
2599
|
+
claudeMeta?.parentToolUseId &&
|
|
2600
|
+
session.emittedToolCalls.has(toolCallId)) {
|
|
2601
|
+
recordToolCallParent(session, toolCallId, claudeMeta.parentToolUseId);
|
|
2602
|
+
}
|
|
2566
2603
|
const routedNotification = await subagents.route(notification, sendUpdate, eagerOwnerSessionId);
|
|
2567
2604
|
if (!routedNotification) {
|
|
2568
2605
|
// Native Agent/Task control calls are intentionally not transcript
|
|
@@ -3011,6 +3048,29 @@ export class ClaudeAcpAgent {
|
|
|
3011
3048
|
}
|
|
3012
3049
|
this.syncAwaitingUser(params.sessionId, session);
|
|
3013
3050
|
};
|
|
3051
|
+
/** Ends the display terminal of a command that went to the background, once
|
|
3052
|
+
* its task ends (see `Session.backgroundTerminals`). Claude Code reports no
|
|
3053
|
+
* exit code for it, and `completed` does not mean 0: Claude Code accepts
|
|
3054
|
+
* some non-zero codes as a success, such as grep's 1. */
|
|
3055
|
+
const endBackgroundTerminal = async (taskId) => {
|
|
3056
|
+
const terminal = session.backgroundTerminals?.get(taskId);
|
|
3057
|
+
if (!terminal)
|
|
3058
|
+
return;
|
|
3059
|
+
session.backgroundTerminals.delete(taskId);
|
|
3060
|
+
await sendUpdate({
|
|
3061
|
+
sessionId: params.sessionId,
|
|
3062
|
+
update: {
|
|
3063
|
+
sessionUpdate: "tool_call_update",
|
|
3064
|
+
toolCallId: terminal.toolCallId,
|
|
3065
|
+
_meta: {
|
|
3066
|
+
...(terminal.parentToolUseId
|
|
3067
|
+
? { claudeCode: { parentToolUseId: terminal.parentToolUseId } }
|
|
3068
|
+
: {}),
|
|
3069
|
+
terminal_exit: { terminal_id: terminal.toolCallId, exit_code: null, signal: null },
|
|
3070
|
+
},
|
|
3071
|
+
},
|
|
3072
|
+
});
|
|
3073
|
+
};
|
|
3014
3074
|
/** Settles a task. Its request may still be open, and an agent without a
|
|
3015
3075
|
* record no longer blocks the foreground work, so that is checked again. */
|
|
3016
3076
|
const settleLiveTask = (taskId) => {
|
|
@@ -3087,6 +3147,92 @@ export class ClaudeAcpAgent {
|
|
|
3087
3147
|
await settleActive(outcome);
|
|
3088
3148
|
}
|
|
3089
3149
|
};
|
|
3150
|
+
/** Sends `report`, which ends the tool call `toolCallId` without its result
|
|
3151
|
+
* (see `AcpToolCallRenderer.unfinished`), and forgets the call. Forgetting
|
|
3152
|
+
* comes first, so that a late hook or a second close cannot overwrite the
|
|
3153
|
+
* report. */
|
|
3154
|
+
const closeToolCall = async (toolCallId, report) => {
|
|
3155
|
+
unregisterHookCallback(toolCallId);
|
|
3156
|
+
session.emittedToolCalls.delete(toolCallId);
|
|
3157
|
+
delete session.toolUseCache[toolCallId];
|
|
3158
|
+
session.toolCallFields?.delete(toolCallId);
|
|
3159
|
+
session.dispatchedToolCalls?.delete(toolCallId);
|
|
3160
|
+
session.toolCallParents?.delete(toolCallId);
|
|
3161
|
+
await sendUpdate({ sessionId: params.sessionId, update: report });
|
|
3162
|
+
};
|
|
3163
|
+
/** Whether the subagent that the Agent call `agentToolUseId` started runs in
|
|
3164
|
+
* the background and has not ended. */
|
|
3165
|
+
const runsInBackground = (agentToolUseId) => [...session.liveBackgroundTasks.values()].some((task) => task.isSubagent &&
|
|
3166
|
+
task.backgrounded === true &&
|
|
3167
|
+
!task.endedPerLevel &&
|
|
3168
|
+
task.parentToolUseId === agentToolUseId);
|
|
3169
|
+
/** Closes the tool calls that the work a cancel stopped left open, as that
|
|
3170
|
+
* work ends. Claude Code's interrupt ends each running tool of the main
|
|
3171
|
+
* conversation with an error result, which `sendInterruptedToolResults`
|
|
3172
|
+
* delivers. What is still open here has no result to deliver:
|
|
3173
|
+
* - a tool of a subagent: Claude Code writes its interrupted result only to
|
|
3174
|
+
* the subagent's own transcript, not to the stream;
|
|
3175
|
+
* - a tool call that Claude abandoned before it ran;
|
|
3176
|
+
* - every tool call, when the stream gives no results at all, as when the
|
|
3177
|
+
* force-cancel backstop ends a wedged turn.
|
|
3178
|
+
* A foreground subagent ends with the work that waits on it. A background
|
|
3179
|
+
* subagent keeps its tool calls open until the SDK reports its end, which
|
|
3180
|
+
* Claude Code 2.1.293 does for the ones it stops, before the turn's result
|
|
3181
|
+
* (see `NativeSubagentRuntime.parentCancelled`).
|
|
3182
|
+
* Runs once per turn: a second call, from the other settling path, waits
|
|
3183
|
+
* for the first (see `Turn.closingCancelledToolCalls`). */
|
|
3184
|
+
const closeCancelledToolCalls = (turn) => (turn.closingCancelledToolCalls ??= (async () => {
|
|
3185
|
+
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
|
3186
|
+
for (const toolCallId of [...session.emittedToolCalls]) {
|
|
3187
|
+
const parentToolUseId = session.toolCallParents?.get(toolCallId);
|
|
3188
|
+
if (parentToolUseId !== undefined && runsInBackground(parentToolUseId))
|
|
3189
|
+
continue;
|
|
3190
|
+
const report = renderer.unfinished({
|
|
3191
|
+
toolCallId,
|
|
3192
|
+
ran: session.dispatchedToolCalls?.has(toolCallId) === true,
|
|
3193
|
+
cancelled: true,
|
|
3194
|
+
});
|
|
3195
|
+
// Named as the call's other updates are: the subagent runtime routes the
|
|
3196
|
+
// report with them, to the child session, or to none that has ended.
|
|
3197
|
+
if (parentToolUseId !== undefined)
|
|
3198
|
+
stampParentToolUseId(report, parentToolUseId);
|
|
3199
|
+
await closeToolCall(toolCallId, report);
|
|
3200
|
+
}
|
|
3201
|
+
})());
|
|
3202
|
+
session.closeCancelledToolCalls = closeCancelledToolCalls;
|
|
3203
|
+
/** Delivers the results that the work a cancel stopped sends for tool calls
|
|
3204
|
+
* that the client holds open. Claude Code's interrupt ends each running tool
|
|
3205
|
+
* of the main conversation with an error result, before the result that
|
|
3206
|
+
* ends the turn. The tool call gets that result like any other, so its
|
|
3207
|
+
* final status and `nonExecutionKind` reach the client. Nothing else of the
|
|
3208
|
+
* message is applied: the other effects of a tool result (async tasks, plan
|
|
3209
|
+
* state, the ExitPlanMode flow) belong to the stopped work, which stays
|
|
3210
|
+
* dropped. */
|
|
3211
|
+
const sendInterruptedToolResults = async (message) => {
|
|
3212
|
+
const content = message.message.content;
|
|
3213
|
+
if (!Array.isArray(content))
|
|
3214
|
+
return;
|
|
3215
|
+
const results = content.filter((block) => block.type === "tool_result" && session.emittedToolCalls.has(block.tool_use_id));
|
|
3216
|
+
if (results.length === 0)
|
|
3217
|
+
return;
|
|
3218
|
+
const resultCount = content.filter((block) => block.type === "tool_result").length;
|
|
3219
|
+
for (const notification of toAcpNotifications(results, "user", params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
3220
|
+
clientCapabilities: this.clientCapabilities,
|
|
3221
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
3222
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
3223
|
+
cwd: session.cwd,
|
|
3224
|
+
taskState: session.taskState,
|
|
3225
|
+
emittedToolCalls: session.emittedToolCalls,
|
|
3226
|
+
toolCallFields: toolCallFieldsOf(session),
|
|
3227
|
+
messageId: messageIdForGrouping(message),
|
|
3228
|
+
// `tool_use_result` describes the one result of its message.
|
|
3229
|
+
toolUseResult: results.length === resultCount ? message.tool_use_result : undefined,
|
|
3230
|
+
toolResultMeta: message.tool_result_meta,
|
|
3231
|
+
cancelled: true,
|
|
3232
|
+
})) {
|
|
3233
|
+
await sendUpdate(notification);
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3090
3236
|
/** At the actual turn boundary, preview its checkpoint, settle the active
|
|
3091
3237
|
* turn exactly once, disarm the force-cancel backstop, and drop it from
|
|
3092
3238
|
* the queue. Cancellation and provider failures skip checkpoint I/O. */
|
|
@@ -3112,16 +3258,7 @@ export class ClaudeAcpAgent {
|
|
|
3112
3258
|
* ended meanwhile.
|
|
3113
3259
|
*/
|
|
3114
3260
|
const closeUnfinished = async (toolCallId, ran) => {
|
|
3115
|
-
|
|
3116
|
-
unregisterHookCallback(toolCallId);
|
|
3117
|
-
session.emittedToolCalls.delete(toolCallId);
|
|
3118
|
-
delete session.toolUseCache[toolCallId];
|
|
3119
|
-
session.toolCallFields?.delete(toolCallId);
|
|
3120
|
-
session.dispatchedToolCalls?.delete(toolCallId);
|
|
3121
|
-
await sendUpdate({
|
|
3122
|
-
sessionId: params.sessionId,
|
|
3123
|
-
update: renderer.unfinished({ toolCallId, ran }),
|
|
3124
|
-
});
|
|
3261
|
+
await closeToolCall(toolCallId, renderer.unfinished({ toolCallId, ran }));
|
|
3125
3262
|
// Cancellation can arrive while we await the client update.
|
|
3126
3263
|
if (turn.settled || session.activeTurn !== turn)
|
|
3127
3264
|
return false;
|
|
@@ -3154,6 +3291,15 @@ export class ClaudeAcpAgent {
|
|
|
3154
3291
|
return;
|
|
3155
3292
|
}
|
|
3156
3293
|
}
|
|
3294
|
+
// The client is told that the work a cancel stopped ended (the prompt
|
|
3295
|
+
// response on v1, `idle` on v2) only after every tool call that work
|
|
3296
|
+
// opened has ended.
|
|
3297
|
+
if (result.stopReason === "cancelled" && session.cancelled) {
|
|
3298
|
+
await closeCancelledToolCalls(turn);
|
|
3299
|
+
// `cancelTurns` can settle the turn while the reports are in flight.
|
|
3300
|
+
if (turn.settled || session.activeTurn !== turn)
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3157
3303
|
turn.settling = true;
|
|
3158
3304
|
turn.settlingOutcome = result;
|
|
3159
3305
|
if (reportReason === "notReported") {
|
|
@@ -4064,9 +4210,14 @@ export class ClaudeAcpAgent {
|
|
|
4064
4210
|
this.logger.log(`Ignoring task_notification for task ${message.task_id} from earlier run ${message.run_id}`);
|
|
4065
4211
|
break;
|
|
4066
4212
|
}
|
|
4213
|
+
// In the RFD form, a resumed child runs under the call that
|
|
4214
|
+
// resumed it, and the end of an earlier delegation ends
|
|
4215
|
+
// nothing: the child and its live task belong to the new run.
|
|
4216
|
+
if (subagents.isStaleEnd(message.task_id, message.tool_use_id))
|
|
4217
|
+
break;
|
|
4067
4218
|
// The task settled — no further tool calls can originate
|
|
4068
4219
|
// from it, so its registry entry can be dropped.
|
|
4069
|
-
await subagents.finishTask(message.task_id, message.status, sendUpdate, message.tool_use_id);
|
|
4220
|
+
await subagents.finishTask(message.task_id, message.status, sendUpdate, message.tool_use_id, { reason: message.reason });
|
|
4070
4221
|
await asyncTasks.taskNotification({
|
|
4071
4222
|
task_id: message.task_id,
|
|
4072
4223
|
status: message.status,
|
|
@@ -4078,7 +4229,9 @@ export class ClaudeAcpAgent {
|
|
|
4078
4229
|
subagents.discardPending(message.tool_use_id);
|
|
4079
4230
|
// The subagent streams no more text under this parent.
|
|
4080
4231
|
streamedBlocksByParent.delete(message.tool_use_id);
|
|
4232
|
+
droppedStreams.delete(message.tool_use_id);
|
|
4081
4233
|
}
|
|
4234
|
+
await endBackgroundTerminal(message.task_id);
|
|
4082
4235
|
settleLiveTask(message.task_id);
|
|
4083
4236
|
break;
|
|
4084
4237
|
case "task_updated":
|
|
@@ -4103,10 +4256,13 @@ export class ClaudeAcpAgent {
|
|
|
4103
4256
|
if (message.patch.status === "completed" ||
|
|
4104
4257
|
message.patch.status === "failed" ||
|
|
4105
4258
|
message.patch.status === "killed") {
|
|
4106
|
-
await subagents.finishTask(message.task_id, message.patch.status, sendUpdate);
|
|
4259
|
+
await subagents.finishTask(message.task_id, message.patch.status, sendUpdate, undefined, { error: message.patch.status === "failed" ? message.patch.error : undefined });
|
|
4107
4260
|
const parentToolUseId = session.liveBackgroundTasks.get(message.task_id)?.parentToolUseId;
|
|
4108
|
-
if (parentToolUseId)
|
|
4261
|
+
if (parentToolUseId) {
|
|
4109
4262
|
streamedBlocksByParent.delete(parentToolUseId);
|
|
4263
|
+
droppedStreams.delete(parentToolUseId);
|
|
4264
|
+
}
|
|
4265
|
+
await endBackgroundTerminal(message.task_id);
|
|
4110
4266
|
settleLiveTask(message.task_id);
|
|
4111
4267
|
}
|
|
4112
4268
|
else if (message.patch.status === "running" ||
|
|
@@ -4930,43 +5086,14 @@ export class ClaudeAcpAgent {
|
|
|
4930
5086
|
// reset, and the synthetic-auth/system/local-command paths
|
|
4931
5087
|
// `break` early too. Block indices restart at 0 each message, so
|
|
4932
5088
|
// leftover entries would otherwise collide with this message's
|
|
4933
|
-
// blocks and re-emit (or truncate) already-streamed text.
|
|
4934
|
-
//
|
|
4935
|
-
//
|
|
4936
|
-
//
|
|
4937
|
-
//
|
|
5089
|
+
// blocks and re-emit (or truncate) already-streamed text. Keyed by
|
|
5090
|
+
// the stream, so a subagent's message can't clear the top-level
|
|
5091
|
+
// record. Fires once, before any of this message's blocks, so it
|
|
5092
|
+
// doesn't disturb the mid-message turn-activation path the way
|
|
5093
|
+
// resetting on turn activation would. The record's drop latch
|
|
5094
|
+
// (see below) resets with it.
|
|
4938
5095
|
streamedBlocksOf(message.parent_tool_use_id).length = 0;
|
|
4939
|
-
|
|
4940
|
-
// Accumulate the text/thinking actually streamed live, so the
|
|
4941
|
-
// `assistant` case below can diff its assembled blocks against what
|
|
4942
|
-
// already reached the client as chunks and forward only the
|
|
4943
|
-
// remainder. Each stream (top level or one subagent) keeps its own
|
|
4944
|
-
// record, so a subagent cannot attribute content to another stream.
|
|
4945
|
-
// Contiguous deltas of the same block (same index and type) extend
|
|
4946
|
-
// the current entry; anything else opens a new one.
|
|
4947
|
-
if (message.event.type === "content_block_delta") {
|
|
4948
|
-
const delta = message.event.delta;
|
|
4949
|
-
const chunk = delta.type === "text_delta"
|
|
4950
|
-
? { type: "text", text: delta.text }
|
|
4951
|
-
: delta.type === "thinking_delta"
|
|
4952
|
-
? { type: "thinking", text: delta.thinking }
|
|
4953
|
-
: undefined;
|
|
4954
|
-
// Skip empty deltas (some gateways emit empty thinking chunks —
|
|
4955
|
-
// #793): appending "" is a no-op, but pushing a "" entry would
|
|
4956
|
-
// create a block the consolidated handler's `text.length > 0`
|
|
4957
|
-
// guard can never consume, stalling the diff cursor and
|
|
4958
|
-
// re-emitting the next block as a duplicate.
|
|
4959
|
-
if (chunk?.text) {
|
|
4960
|
-
const index = message.event.index;
|
|
4961
|
-
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
4962
|
-
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
4963
|
-
if (last && last.index === index && last.type === chunk.type) {
|
|
4964
|
-
last.text += chunk.text;
|
|
4965
|
-
}
|
|
4966
|
-
else {
|
|
4967
|
-
streamedBlocks.push({ index, type: chunk.type, text: chunk.text });
|
|
4968
|
-
}
|
|
4969
|
-
}
|
|
5096
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
4970
5097
|
}
|
|
4971
5098
|
if (message.parent_tool_use_id === null &&
|
|
4972
5099
|
(message.event.type === "message_start" || message.event.type === "message_delta")) {
|
|
@@ -5020,7 +5147,71 @@ export class ClaudeAcpAgent {
|
|
|
5020
5147
|
});
|
|
5021
5148
|
}
|
|
5022
5149
|
}
|
|
5023
|
-
|
|
5150
|
+
// After a cancel, output streams to the client until the client is
|
|
5151
|
+
// told that the stopped work ended (see workEndReported), so what
|
|
5152
|
+
// the interrupt flushes before then still goes out, as both
|
|
5153
|
+
// protocol versions allow. After that, while the window after the
|
|
5154
|
+
// cancel is open, streamed text, thinking and tool calls are
|
|
5155
|
+
// dropped like the consolidated messages they belong to (see the
|
|
5156
|
+
// `cancelled` check in the user/assistant case). Otherwise the
|
|
5157
|
+
// answer to a prompt the cancel abandoned, which Claude Code had
|
|
5158
|
+
// already taken off its queue, would stream to the client after the
|
|
5159
|
+
// cancel was reported.
|
|
5160
|
+
//
|
|
5161
|
+
// Where the consolidated message delivers the text (the same
|
|
5162
|
+
// condition as in the user/assistant case), a stream that loses a
|
|
5163
|
+
// delta keeps dropping until the record below resets: at the next
|
|
5164
|
+
// message, or when a consolidated message consumes the record. If
|
|
5165
|
+
// the next prompt's echo ends the window mid-block, the record then
|
|
5166
|
+
// stays empty and the consolidated message delivers the block's
|
|
5167
|
+
// whole text once. A record holding only the block's tail is not a
|
|
5168
|
+
// prefix of the text, so the text would be sent again (see
|
|
5169
|
+
// unstreamedRemainder).
|
|
5170
|
+
const streamKey = message.parent_tool_use_id ?? "";
|
|
5171
|
+
if (session.cancelled && this.workEndReported(params.sessionId, session)) {
|
|
5172
|
+
if (message.parent_tool_use_id === null || forwardsSubagentText()) {
|
|
5173
|
+
droppedStreams.add(streamKey);
|
|
5174
|
+
}
|
|
5175
|
+
break;
|
|
5176
|
+
}
|
|
5177
|
+
if (droppedStreams.has(streamKey)) {
|
|
5178
|
+
break;
|
|
5179
|
+
}
|
|
5180
|
+
// Accumulate the text/thinking actually streamed live, so the
|
|
5181
|
+
// `assistant` case below can diff its assembled blocks against what
|
|
5182
|
+
// already reached the client as chunks and forward only the
|
|
5183
|
+
// remainder. Each stream (top level or one subagent) keeps its own
|
|
5184
|
+
// record, so a subagent cannot attribute content to another stream.
|
|
5185
|
+
// Contiguous deltas of the same block (same index and type) extend
|
|
5186
|
+
// the current entry; anything else opens a new one.
|
|
5187
|
+
if (message.event.type === "content_block_delta") {
|
|
5188
|
+
const delta = message.event.delta;
|
|
5189
|
+
const chunk = delta.type === "text_delta"
|
|
5190
|
+
? { type: "text", text: delta.text }
|
|
5191
|
+
: delta.type === "thinking_delta"
|
|
5192
|
+
? { type: "thinking", text: delta.thinking }
|
|
5193
|
+
: undefined;
|
|
5194
|
+
// Skip empty deltas (some gateways emit empty thinking chunks —
|
|
5195
|
+
// #793): appending "" is a no-op, but pushing a "" entry would
|
|
5196
|
+
// create a block the consolidated handler's `text.length > 0`
|
|
5197
|
+
// guard can never consume, stalling the diff cursor and
|
|
5198
|
+
// re-emitting the next block as a duplicate.
|
|
5199
|
+
if (chunk?.text) {
|
|
5200
|
+
const index = message.event.index;
|
|
5201
|
+
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
5202
|
+
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
5203
|
+
if (last && last.index === index && last.type === chunk.type) {
|
|
5204
|
+
last.text += chunk.text;
|
|
5205
|
+
}
|
|
5206
|
+
else {
|
|
5207
|
+
streamedBlocks.push({ index, type: chunk.type, text: chunk.text });
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
// The stream sees a tool_use first, so the tool's PostToolUse hook
|
|
5212
|
+
// is registered here, and publishes through this client: a
|
|
5213
|
+
// subagent's hook update must reach the child session too.
|
|
5214
|
+
for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
5024
5215
|
clientCapabilities: this.clientCapabilities,
|
|
5025
5216
|
toolCallCapabilities: this.toolCallCapabilities,
|
|
5026
5217
|
cwd: session.cwd,
|
|
@@ -5153,6 +5344,13 @@ export class ClaudeAcpAgent {
|
|
|
5153
5344
|
}
|
|
5154
5345
|
}
|
|
5155
5346
|
if (session.cancelled) {
|
|
5347
|
+
// The work the cancel stopped ends its tools with error results.
|
|
5348
|
+
// Until the client is told that the work ended, they close the
|
|
5349
|
+
// tool calls it opened. Everything else that work sends is
|
|
5350
|
+
// dropped, and so is everything after that report.
|
|
5351
|
+
if (message.type === "user" && !this.workEndReported(params.sessionId, session)) {
|
|
5352
|
+
await sendInterruptedToolResults(message);
|
|
5353
|
+
}
|
|
5156
5354
|
break;
|
|
5157
5355
|
}
|
|
5158
5356
|
// Synthetic assistant frames carry the CLI's local-command output.
|
|
@@ -5284,6 +5482,7 @@ export class ClaudeAcpAgent {
|
|
|
5284
5482
|
const streamed = streamedBlocksOf(message.parent_tool_use_id);
|
|
5285
5483
|
content = unstreamedRemainder(message.message.content, streamed);
|
|
5286
5484
|
streamed.length = 0;
|
|
5485
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
5287
5486
|
}
|
|
5288
5487
|
else if (message.type === "assistant") {
|
|
5289
5488
|
// Nested text/thinking stays internal for a client that does
|
|
@@ -5296,19 +5495,41 @@ export class ClaudeAcpAgent {
|
|
|
5296
5495
|
}
|
|
5297
5496
|
const acceptedPlanToolUseId = observeExitPlanToolResults(message, content, session);
|
|
5298
5497
|
let backgroundedToolCalls = new Set();
|
|
5498
|
+
let runningTerminal;
|
|
5299
5499
|
if (message.type === "user") {
|
|
5300
5500
|
rememberResolvedToolNames(session, content);
|
|
5301
5501
|
const backgroundBashTask = backgroundBashTaskFromToolResult(content, message.tool_use_result, session.toolUseCache);
|
|
5302
5502
|
if (backgroundBashTask)
|
|
5303
5503
|
await asyncTasks.taskBackgrounded(backgroundBashTask);
|
|
5504
|
+
// The command's terminal runs until its task ends, unless the
|
|
5505
|
+
// task has ended already (a fast command can end before the tool
|
|
5506
|
+
// result comes).
|
|
5507
|
+
const terminalId = backgroundBashTask?.toolCallId;
|
|
5508
|
+
if (terminalId &&
|
|
5509
|
+
this.toolCallCapabilities.terminalOutput &&
|
|
5510
|
+
!this.toolCallCapabilities.air.client &&
|
|
5511
|
+
session.liveBackgroundTasks.has(backgroundBashTask.taskId)) {
|
|
5512
|
+
runningTerminal = terminalId;
|
|
5513
|
+
(session.backgroundTerminals ??= new Map()).set(backgroundBashTask.taskId, {
|
|
5514
|
+
toolCallId: terminalId,
|
|
5515
|
+
...(message.parent_tool_use_id
|
|
5516
|
+
? { parentToolUseId: message.parent_tool_use_id }
|
|
5517
|
+
: {}),
|
|
5518
|
+
});
|
|
5519
|
+
}
|
|
5304
5520
|
await asyncTasks.toolResults(content);
|
|
5305
5521
|
backgroundedToolCalls = backgroundedBashToolCallIds(content, session.toolUseCache, asyncTasks, backgroundBashTask);
|
|
5306
5522
|
const resumedAgentId = resumedNativeSubagentId(message.tool_use_result);
|
|
5307
5523
|
if (resumedAgentId) {
|
|
5308
|
-
|
|
5309
|
-
await subagents.taskResumed(resumedAgentId, sendUpdate, sendMessageResumePrompt(session.toolUseCache, resumedAgentId, Array.isArray(content)
|
|
5524
|
+
const resume = sendMessageResume(session.toolUseCache, resumedAgentId, Array.isArray(content)
|
|
5310
5525
|
? content.flatMap((block) => block.type === "tool_result" ? [block.tool_use_id] : [])
|
|
5311
|
-
: [])
|
|
5526
|
+
: []);
|
|
5527
|
+
// In the RFD form the result can arrive after the run it
|
|
5528
|
+
// started ended, and then resumes nothing.
|
|
5529
|
+
if (!subagents.isEndedDelegation(resumedAgentId, resume?.toolUseId)) {
|
|
5530
|
+
resumeLiveTask(resumedAgentId);
|
|
5531
|
+
await subagents.sendMessageResumed(resumedAgentId, sendUpdate, resume?.text, resume?.toolUseId);
|
|
5532
|
+
}
|
|
5312
5533
|
}
|
|
5313
5534
|
}
|
|
5314
5535
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
@@ -5331,7 +5552,7 @@ export class ClaudeAcpAgent {
|
|
|
5331
5552
|
// filtered out of `content` above; blocks that do pass through
|
|
5332
5553
|
// (e.g. a subagent image) carry the stamped parentToolUseId
|
|
5333
5554
|
// meta and are excluded there.
|
|
5334
|
-
await sendUpdate(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled));
|
|
5555
|
+
await sendUpdate(withRunningTerminal(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled), runningTerminal));
|
|
5335
5556
|
}
|
|
5336
5557
|
break;
|
|
5337
5558
|
}
|
|
@@ -5355,10 +5576,14 @@ export class ClaudeAcpAgent {
|
|
|
5355
5576
|
if (toolCallId === null || !session.emittedToolCalls.has(toolCallId)) {
|
|
5356
5577
|
break;
|
|
5357
5578
|
}
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5579
|
+
// The beat is in the subagent whose Agent call is
|
|
5580
|
+
// `parent_tool_use_id`, unless it reports on that Agent call
|
|
5581
|
+
// itself: the Agent call's own heartbeat falls back to its id, and
|
|
5582
|
+
// a tool call is never its own parent.
|
|
5583
|
+
const subagentParentToolUseId = message.parent_tool_use_id &&
|
|
5584
|
+
message.parent_tool_use_id !== toolCallId &&
|
|
5585
|
+
[...session.liveBackgroundTasks.values()].some((task) => task.isSubagent && task.parentToolUseId === message.parent_tool_use_id)
|
|
5586
|
+
? message.parent_tool_use_id
|
|
5362
5587
|
: undefined;
|
|
5363
5588
|
// `tool_name` names the tool that reports the beat. When the beat
|
|
5364
5589
|
// falls back to the parent call, AIR gets the name of that call,
|
|
@@ -5581,17 +5806,29 @@ export class ClaudeAcpAgent {
|
|
|
5581
5806
|
// their compaction is running. Close it before ANY cancelled prompt
|
|
5582
5807
|
// resolves, not just the active-turn idle/backstop paths.
|
|
5583
5808
|
await session.contextCompaction?.interrupt();
|
|
5809
|
+
// A cancel drops the state of each tool call (the session that a
|
|
5810
|
+
// permission request emitted it in, the fields the client holds, the hook
|
|
5811
|
+
// callbacks), even when publishing fails. AIR's draft ends every child
|
|
5812
|
+
// here, so a later update of an ended child's tool call is then dropped
|
|
5813
|
+
// instead of reaching that child's session. The RFD form keeps the state:
|
|
5814
|
+
// a background subagent can keep running after a cancel
|
|
5815
|
+
// (agentclientprotocol/agent-client-protocol#2308), and its tool call that
|
|
5816
|
+
// a permission request emitted on the root before the child was announced
|
|
5817
|
+
// must stay on the root, which the RFD requires. Each tool call drops its
|
|
5818
|
+
// own state when it ends.
|
|
5584
5819
|
try {
|
|
5585
|
-
await session.nativeSubagentRuntime?.
|
|
5820
|
+
await session.nativeSubagentRuntime?.parentCancelled(session.nativeSubagentDeliver ??
|
|
5586
5821
|
(async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification))));
|
|
5587
5822
|
}
|
|
5588
5823
|
catch (error) {
|
|
5589
5824
|
this.logger.error(`Session ${params.sessionId}: failed to publish cancelled subagent state`, error);
|
|
5590
5825
|
}
|
|
5591
5826
|
finally {
|
|
5592
|
-
session.
|
|
5593
|
-
|
|
5594
|
-
|
|
5827
|
+
if (session.nativeSubagentRuntime?.form !== "rfd") {
|
|
5828
|
+
session.eagerToolCallSessions?.clear();
|
|
5829
|
+
session.toolCallFields?.clear();
|
|
5830
|
+
clearHookCallbacks(params.sessionId);
|
|
5831
|
+
}
|
|
5595
5832
|
}
|
|
5596
5833
|
// The user messages this cancel abandons while Claude Code may still have
|
|
5597
5834
|
// them queued: they are dropped from its queue before the interrupt.
|
|
@@ -5705,7 +5942,8 @@ export class ClaudeAcpAgent {
|
|
|
5705
5942
|
// permission request) is left to that cycle's interrupted result, like any
|
|
5706
5943
|
// live turn, or to the idle when no result comes. The captured outcome
|
|
5707
5944
|
// preserves usage and metadata (issue #844), while reporter state makes a
|
|
5708
|
-
// late checkpoint response harmless.
|
|
5945
|
+
// late checkpoint response harmless. The turn's open tool calls close
|
|
5946
|
+
// before its response, as in the consumer's settle.
|
|
5709
5947
|
// Since CLI 2.1.270, Claude Code reports `running` for as long as
|
|
5710
5948
|
// background agents live, so the session state cannot tell a live
|
|
5711
5949
|
// followup from a hold. With request markers, a followup is live when one
|
|
@@ -5718,7 +5956,10 @@ export class ClaudeAcpAgent {
|
|
|
5718
5956
|
if (active &&
|
|
5719
5957
|
(active.settling || (isHeldOpen(active) && !followupLive)) &&
|
|
5720
5958
|
(active.deferredSettle ?? active.settlingOutcome)) {
|
|
5959
|
+
const closingToolCalls = session.closeCancelledToolCalls?.(active);
|
|
5721
5960
|
session.fileChangeReporter?.finish(active.fileChangeReport, "cancelled");
|
|
5961
|
+
// Settled before the reports are awaited, so that the consumer, which
|
|
5962
|
+
// keeps running meanwhile, cannot settle the turn a second time.
|
|
5722
5963
|
active.settled = true;
|
|
5723
5964
|
// Mirror settleActive's invariants (it is consumer-scoped and
|
|
5724
5965
|
// unreachable from here): disarm the backstop — none should be
|
|
@@ -5750,7 +5991,15 @@ export class ClaudeAcpAgent {
|
|
|
5750
5991
|
if (session.lastSessionState !== "idle") {
|
|
5751
5992
|
session.owedTrailingIdles++;
|
|
5752
5993
|
}
|
|
5753
|
-
|
|
5994
|
+
try {
|
|
5995
|
+
await closingToolCalls;
|
|
5996
|
+
}
|
|
5997
|
+
catch (error) {
|
|
5998
|
+
this.logger.error(`Session ${params.sessionId}: failed to publish cancelled tool state`, error);
|
|
5999
|
+
}
|
|
6000
|
+
finally {
|
|
6001
|
+
active.resolve(cancelledOutcome(session, active));
|
|
6002
|
+
}
|
|
5754
6003
|
}
|
|
5755
6004
|
}
|
|
5756
6005
|
// A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
|
|
@@ -6053,7 +6302,12 @@ export class ClaudeAcpAgent {
|
|
|
6053
6302
|
// A replay rebuilds the client view, so its plan goes out again.
|
|
6054
6303
|
if (session?.taskState)
|
|
6055
6304
|
forgetPublishedTaskPlan(session.taskState);
|
|
6056
|
-
|
|
6305
|
+
// A subagent's text replays onto the root only for a client that gets it
|
|
6306
|
+
// there live: one without native subagent sessions that asked for the
|
|
6307
|
+
// flattened transcript. With native sessions the text goes to the child
|
|
6308
|
+
// session, which AIR's replay restores and the RFD form leaves out for now.
|
|
6309
|
+
const forwardSubagentText = !clientSupportsSubagents(this.clientCapabilities) &&
|
|
6310
|
+
(session?.forwardSubagentText ?? supportsSubagentTranscript(this.clientCapabilities));
|
|
6057
6311
|
const supportsTypedFailures = supportsAirSessionFailures(this.clientCapabilities);
|
|
6058
6312
|
const activeUsageLimit = supportsTypedFailures ? activeUsageLimitMessage(messages) : undefined;
|
|
6059
6313
|
const sessionFailures = session && supportsTypedFailures
|
|
@@ -6072,7 +6326,11 @@ export class ClaudeAcpAgent {
|
|
|
6072
6326
|
})
|
|
6073
6327
|
: undefined;
|
|
6074
6328
|
let replayTurnId;
|
|
6075
|
-
|
|
6329
|
+
// AIR's draft replays each child under a replay id. A client on the RFD
|
|
6330
|
+
// form gets each child under the session id it had live (see
|
|
6331
|
+
// `RfdSubagentReplay`).
|
|
6332
|
+
const form = subagentForm(this.clientCapabilities);
|
|
6333
|
+
const nativeReplayEnabled = form === "air";
|
|
6076
6334
|
const replayCompactionUpdates = clientSupportsCompactionUpdates(this.clientCapabilities);
|
|
6077
6335
|
const replayTerminalStates = new Map();
|
|
6078
6336
|
const replayChildren = new Map();
|
|
@@ -6175,8 +6433,17 @@ export class ClaudeAcpAgent {
|
|
|
6175
6433
|
// Only a history with an Agent or Task launch has subagent transcripts to read.
|
|
6176
6434
|
const subagents = nativeReplayEnabled && replayChildren.size > 0
|
|
6177
6435
|
? await subagentHistory(sessionId)
|
|
6178
|
-
: {
|
|
6436
|
+
: { agents: new Map() };
|
|
6179
6437
|
const replayedSubagents = new Set();
|
|
6438
|
+
const rfdHistory = form === "rfd" &&
|
|
6439
|
+
messages.some((message) => subagentLaunchIds(message.message?.content)
|
|
6440
|
+
.length > 0)
|
|
6441
|
+
? await subagentHistory(sessionId)
|
|
6442
|
+
: { agents: new Map() };
|
|
6443
|
+
const rfdReplay = form === "rfd"
|
|
6444
|
+
? new RfdSubagentReplay(rfdHistory, (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)))
|
|
6445
|
+
: undefined;
|
|
6446
|
+
rfdReplay?.scan(messages, sessionId);
|
|
6180
6447
|
// History persists no task_started frame, so a background task gets its
|
|
6181
6448
|
// spawn from the notification of its stop, with the tool call that
|
|
6182
6449
|
// started it. A task still running at the end of the history gets no
|
|
@@ -6195,6 +6462,8 @@ export class ClaudeAcpAgent {
|
|
|
6195
6462
|
});
|
|
6196
6463
|
// The replay counterpart of the live `task_notification` frame.
|
|
6197
6464
|
const restoreTaskNotification = async (notification) => {
|
|
6465
|
+
if (rfdReplay?.taskNotification(notification.task_id, notification.status))
|
|
6466
|
+
return;
|
|
6198
6467
|
const toolUseId = notification.tool_use_id;
|
|
6199
6468
|
if (!toolUseId)
|
|
6200
6469
|
return;
|
|
@@ -6221,7 +6490,8 @@ export class ClaudeAcpAgent {
|
|
|
6221
6490
|
});
|
|
6222
6491
|
await replayAsyncTasks.taskNotification(notification);
|
|
6223
6492
|
};
|
|
6224
|
-
|
|
6493
|
+
/** Replays `message`, from the transcript of `child` when it is set (the RFD form). */
|
|
6494
|
+
const replayMessage = async (message, child) => {
|
|
6225
6495
|
// A long history is sent in order, but with event-loop turns in between,
|
|
6226
6496
|
// so other sessions and incoming requests are not starved meanwhile.
|
|
6227
6497
|
const pause = messageLoopYielder.maybeYield();
|
|
@@ -6230,6 +6500,10 @@ export class ClaudeAcpAgent {
|
|
|
6230
6500
|
if (pending?.stopped || isReplayHiddenMetaMessage(message)) {
|
|
6231
6501
|
return;
|
|
6232
6502
|
}
|
|
6503
|
+
// In the RFD form a child's history comes from its own transcript, so a
|
|
6504
|
+
// subagent message that the session transcript stores inline is skipped.
|
|
6505
|
+
if (rfdReplay && !child && parentToolUseIdOf(message))
|
|
6506
|
+
return;
|
|
6233
6507
|
if (message.type === "user" &&
|
|
6234
6508
|
message.parent_tool_use_id === null &&
|
|
6235
6509
|
typeof message.uuid === "string" &&
|
|
@@ -6269,11 +6543,14 @@ export class ClaudeAcpAgent {
|
|
|
6269
6543
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
6270
6544
|
let content = message.message.content;
|
|
6271
6545
|
const parentToolUseId = parentToolUseIdOf(message);
|
|
6272
|
-
const replayTargetSessionId =
|
|
6273
|
-
?
|
|
6274
|
-
:
|
|
6546
|
+
const replayTargetSessionId = child
|
|
6547
|
+
? child.sessionId
|
|
6548
|
+
: nativeReplayEnabled && parentToolUseId
|
|
6549
|
+
? await announceReplayChild(parentToolUseId)
|
|
6550
|
+
: sessionId;
|
|
6275
6551
|
if (message.type === "assistant" &&
|
|
6276
6552
|
parentToolUseId &&
|
|
6553
|
+
!child &&
|
|
6277
6554
|
!nativeReplayEnabled &&
|
|
6278
6555
|
!forwardSubagentText) {
|
|
6279
6556
|
content = stripSubagentTextAndThinking(content);
|
|
@@ -6287,6 +6564,9 @@ export class ClaudeAcpAgent {
|
|
|
6287
6564
|
}
|
|
6288
6565
|
// Live, the prompt loop skips this record and the SDK frame reports the stop.
|
|
6289
6566
|
content = isTaskNotificationRecord(message) ? null : stripLocalCommandMetadata(content);
|
|
6567
|
+
if (content !== null && child && rfdReplay) {
|
|
6568
|
+
content = await rfdReplay.childContent(child, message, content);
|
|
6569
|
+
}
|
|
6290
6570
|
if (content === null)
|
|
6291
6571
|
return;
|
|
6292
6572
|
}
|
|
@@ -6358,32 +6638,37 @@ export class ClaudeAcpAgent {
|
|
|
6358
6638
|
if (nativeReplayEnabled && message.type === "assistant") {
|
|
6359
6639
|
for (const toolUseId of subagentLaunchIds(content)) {
|
|
6360
6640
|
await announceReplayChild(toolUseId);
|
|
6361
|
-
const agentId = subagents.
|
|
6641
|
+
const agentId = subagents.agents.get(toolUseId)?.agentId;
|
|
6362
6642
|
if (!agentId || replayedSubagents.has(agentId))
|
|
6363
6643
|
continue;
|
|
6364
6644
|
replayedSubagents.add(agentId);
|
|
6365
|
-
|
|
6366
|
-
try {
|
|
6367
|
-
// With the project directory, the SDK skips its search of every project.
|
|
6368
|
-
// A session opened from another directory falls back to that search.
|
|
6369
|
-
if (subagents.dir) {
|
|
6370
|
-
childMessages = await getSubagentMessages(sessionId, agentId, { dir: subagents.dir });
|
|
6371
|
-
}
|
|
6372
|
-
if (childMessages.length === 0) {
|
|
6373
|
-
childMessages = await getSubagentMessages(sessionId, agentId);
|
|
6374
|
-
}
|
|
6375
|
-
}
|
|
6376
|
-
catch (error) {
|
|
6377
|
-
this.logger.error(`Failed to read the history of subagent ${agentId}:`, error);
|
|
6378
|
-
}
|
|
6645
|
+
const childMessages = await readSubagentMessages(sessionId, agentId, subagents, this.logger);
|
|
6379
6646
|
registerLaunches(childMessages);
|
|
6380
6647
|
for (const childMessage of childMessages)
|
|
6381
6648
|
await replayMessage(childMessage);
|
|
6382
6649
|
}
|
|
6383
6650
|
}
|
|
6651
|
+
// The RFD form announces a child right after its launch, then sends its
|
|
6652
|
+
// own transcript to it.
|
|
6653
|
+
if (rfdReplay && message.type === "assistant") {
|
|
6654
|
+
for (const toolUseId of subagentLaunchIds(content)) {
|
|
6655
|
+
const launched = rfdReplay.childOfLaunch(toolUseId);
|
|
6656
|
+
if (!launched || launched.announced)
|
|
6657
|
+
continue;
|
|
6658
|
+
await rfdReplay.announce(launched);
|
|
6659
|
+
const childMessages = await readSubagentMessages(sessionId, launched.sessionId, rfdHistory, this.logger);
|
|
6660
|
+
rfdReplay.scan(childMessages, launched.sessionId);
|
|
6661
|
+
for (const childMessage of childMessages)
|
|
6662
|
+
await replayMessage(childMessage, launched);
|
|
6663
|
+
await rfdReplay.childReplayed(launched, childMessages);
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6384
6666
|
};
|
|
6385
6667
|
for (const message of messages)
|
|
6386
6668
|
await replayMessage(message);
|
|
6669
|
+
if (rfdReplay && !pending?.stopped) {
|
|
6670
|
+
await rfdReplay.finish(clientSupportsNotices(this.clientCapabilities), sessionId);
|
|
6671
|
+
}
|
|
6387
6672
|
if (nativeReplayEnabled && !pending?.stopped) {
|
|
6388
6673
|
// Claude history persists sidechain messages and Agent/Task tool uses,
|
|
6389
6674
|
// but not task_started/task_updated lifecycle frames. Recover terminal
|
|
@@ -6418,6 +6703,16 @@ export class ClaudeAcpAgent {
|
|
|
6418
6703
|
const response = await this.client.writeTextFile(params);
|
|
6419
6704
|
return response;
|
|
6420
6705
|
}
|
|
6706
|
+
/** Runs `request`, a request that `requestSessionId` sends for the session
|
|
6707
|
+
* `ownerSessionId`. When that is a subagent's child session, the child
|
|
6708
|
+
* reports that it awaits the user while the request is open (the RFD form,
|
|
6709
|
+
* see `NativeSubagentRuntime.awaitingUser`). */
|
|
6710
|
+
withChildAwaitingUser(ownerSessionId, requestSessionId, request) {
|
|
6711
|
+
const runtime = this.sessions[ownerSessionId]?.nativeSubagentRuntime;
|
|
6712
|
+
return runtime && requestSessionId !== ownerSessionId
|
|
6713
|
+
? runtime.awaitingUser(requestSessionId, request)
|
|
6714
|
+
: request();
|
|
6715
|
+
}
|
|
6421
6716
|
/** Mark a client request of the subagent `agentID` (absent for the main
|
|
6422
6717
|
* conversation) as awaiting user input for exactly the lifetime of its
|
|
6423
6718
|
* promise (see `Session.openUserInputs`). Steering consults the open
|
|
@@ -6512,6 +6807,13 @@ export class ClaudeAcpAgent {
|
|
|
6512
6807
|
return;
|
|
6513
6808
|
this.startForeground(sessionId, AUTONOMOUS_CYCLE);
|
|
6514
6809
|
}
|
|
6810
|
+
/** Whether the client has been told that the session's work ended: on v2 by
|
|
6811
|
+
* an idle state, so no foreground work is open, including a cycle that
|
|
6812
|
+
* Claude Code started on its own; on v1 by the response to the turn's
|
|
6813
|
+
* prompt, so no turn is active. */
|
|
6814
|
+
workEndReported(sessionId, session) {
|
|
6815
|
+
return this.foregroundEvents ? !this.foregroundWork.has(sessionId) : !session.activeTurn;
|
|
6816
|
+
}
|
|
6515
6817
|
/** End the session's model cycle that Claude Code started on its own, if one
|
|
6516
6818
|
* runs. */
|
|
6517
6819
|
endAutonomousCycle(sessionId, end) {
|
|
@@ -6541,7 +6843,7 @@ export class ClaudeAcpAgent {
|
|
|
6541
6843
|
// The request goes to a subagent's child session for a tool call of that
|
|
6542
6844
|
// subagent, but it blocks the turn of the owner session.
|
|
6543
6845
|
try {
|
|
6544
|
-
return await this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID);
|
|
6846
|
+
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, () => this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID));
|
|
6545
6847
|
}
|
|
6546
6848
|
catch (error) {
|
|
6547
6849
|
if (signal.aborted) {
|
|
@@ -6581,6 +6883,8 @@ export class ClaudeAcpAgent {
|
|
|
6581
6883
|
return;
|
|
6582
6884
|
}
|
|
6583
6885
|
session.emittedToolCalls.add(toolCallId);
|
|
6886
|
+
if (parentToolUseId)
|
|
6887
|
+
recordToolCallParent(session, toolCallId, parentToolUseId);
|
|
6584
6888
|
// SandboxNetworkAccess is a permission-only callback, not an executing
|
|
6585
6889
|
// tool: the SDK does not send a tool_result for its synthetic id.
|
|
6586
6890
|
if (!parentToolUseId &&
|
|
@@ -6605,6 +6909,7 @@ export class ClaudeAcpAgent {
|
|
|
6605
6909
|
session.emittedToolCalls.delete(toolCallId);
|
|
6606
6910
|
session.eagerToolCallSessions?.delete(toolCallId);
|
|
6607
6911
|
session.toolCallFields?.delete(toolCallId);
|
|
6912
|
+
session.toolCallParents?.delete(toolCallId);
|
|
6608
6913
|
throw error;
|
|
6609
6914
|
}
|
|
6610
6915
|
}
|
|
@@ -6879,7 +7184,7 @@ export class ClaudeAcpAgent {
|
|
|
6879
7184
|
const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
6880
7185
|
let response;
|
|
6881
7186
|
try {
|
|
6882
|
-
response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal), agentID);
|
|
7187
|
+
response = await this.withChildAwaitingUser(sessionId, requestSessionId, () => this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal), agentID));
|
|
6883
7188
|
}
|
|
6884
7189
|
catch (error) {
|
|
6885
7190
|
// A cancellation we requested (signal aborted) settles as an aborted tool
|
|
@@ -7750,6 +8055,8 @@ export class ClaudeAcpAgent {
|
|
|
7750
8055
|
prompt: input,
|
|
7751
8056
|
options,
|
|
7752
8057
|
});
|
|
8058
|
+
this.useAuthStatusSettings(settingsManager, configuredSettingsObject, userProvidedOptions?.env);
|
|
8059
|
+
const authSettingsEpoch = this.authEpoch;
|
|
7753
8060
|
timing.phase("prepare-query");
|
|
7754
8061
|
// `query()` spawns the CLI at once. Any throw between here and the
|
|
7755
8062
|
// registration in `this.sessions` would leave that child process running
|
|
@@ -7772,7 +8079,9 @@ export class ClaudeAcpAgent {
|
|
|
7772
8079
|
// Publish the identity BEFORE the guard can refuse this session. A
|
|
7773
8080
|
// refusal is exactly when the client most needs to know which account it
|
|
7774
8081
|
// was refused for.
|
|
7775
|
-
this.
|
|
8082
|
+
if (authSettingsEpoch === this.authEpoch) {
|
|
8083
|
+
this.publishSessionAccountIdentity(initializationResult.account, settingsManager.getApiKeyHelper(configuredSettingsObject));
|
|
8084
|
+
}
|
|
7776
8085
|
// Shared with the per-turn guard, so "warn once" spans the whole session.
|
|
7777
8086
|
const claudeSubscriptionGuard = {};
|
|
7778
8087
|
if (this.claudeSubscriptionGuardActive()) {
|
|
@@ -7974,7 +8283,7 @@ export class ClaudeAcpAgent {
|
|
|
7974
8283
|
messageIdToUuid: creationOpts.replayState?.messageIdToUuid ?? new Map(),
|
|
7975
8284
|
sessionFailureState: creationOpts.replayState?.sessionFailureState ?? createSessionFailureState(),
|
|
7976
8285
|
claudeSubscriptionGuard,
|
|
7977
|
-
accountKind: fromAccountInfo(initializationResult.account)?.kind,
|
|
8286
|
+
accountKind: fromAccountInfo(initializationResult.account, settingsManager.getApiKeyHelper(configuredSettingsObject))?.kind,
|
|
7978
8287
|
fileChangeReporter,
|
|
7979
8288
|
};
|
|
7980
8289
|
timing.phase("register");
|
|
@@ -8126,27 +8435,34 @@ function cancelledOutcome(session, turn) {
|
|
|
8126
8435
|
}
|
|
8127
8436
|
/** Whether the model request that `status` (a `status: requesting` frame)
|
|
8128
8437
|
* starts may belong to the work a cancel stopped. It may while:
|
|
8129
|
-
* - a turn is unsettled: the stopped
|
|
8130
|
-
*
|
|
8131
|
-
* cancel awaited is recorded only when the cancel sweeps the queue) or
|
|
8132
|
-
* whose echo ends the cancel right after its request anyway;
|
|
8438
|
+
* - a turn in {@link Session.cancelledPrompts} is unsettled: the stopped
|
|
8439
|
+
* turn, whose result is still to come;
|
|
8133
8440
|
* - the orphan accounting still expects the result of an abandoned prompt
|
|
8134
8441
|
* that Claude Code may still run, which it can fold into its own
|
|
8135
8442
|
* follow-up after that follow-up's first request;
|
|
8136
8443
|
* - the request serves one of {@link Session.cancelledPrompts}, as an
|
|
8137
8444
|
* abandoned prompt's request does even when the interrupt receipt left it
|
|
8138
|
-
* out of the orphan accounting
|
|
8445
|
+
* out of the orphan accounting;
|
|
8446
|
+
* - the request serves an unsettled prompt. One that arrived while the cancel
|
|
8447
|
+
* awaited is recorded only when the cancel sweeps the queue, and its echo
|
|
8448
|
+
* ends the cancel right after its request when the cancel does not.
|
|
8449
|
+
* A prompt that is merely queued does not make another request belong to
|
|
8450
|
+
* the stopped work: a follow-up that Claude Code runs before a prompt sent
|
|
8451
|
+
* after the cancel names no prompt. */
|
|
8139
8452
|
function servesCancelledWork(session, status) {
|
|
8140
8453
|
const cancelled = session.cancelledPrompts;
|
|
8141
8454
|
if (!cancelled)
|
|
8142
8455
|
return false;
|
|
8143
|
-
|
|
8456
|
+
const unsettled = (session.turnQueue ?? []).filter((turn) => !turn.settled);
|
|
8457
|
+
if (unsettled.some((turn) => cancelled.has(turn.promptUuid)))
|
|
8144
8458
|
return true;
|
|
8145
8459
|
if ((session.pendingOrphanResults ?? 0) > 0 || (session.orphanCommands?.size ?? 0) > 0) {
|
|
8146
8460
|
return true;
|
|
8147
8461
|
}
|
|
8148
8462
|
const served = status.user_message_uuids;
|
|
8149
|
-
return (Array.isArray(served) &&
|
|
8463
|
+
return (Array.isArray(served) &&
|
|
8464
|
+
served.some((uuid) => typeof uuid === "string" &&
|
|
8465
|
+
(cancelled.has(uuid) || unsettled.some((turn) => turn.promptUuid === uuid))));
|
|
8150
8466
|
}
|
|
8151
8467
|
/** `_meta.quota` for a prompt response: what the turn spent, shaped like
|
|
8152
8468
|
* codex-acp's (snake_case container keys, camelCase counters) so a client
|
|
@@ -8872,6 +9188,18 @@ function isTaskTool(toolName) {
|
|
|
8872
9188
|
function shouldEmitToolCall(toolName) {
|
|
8873
9189
|
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
|
8874
9190
|
}
|
|
9191
|
+
const MAX_TOOL_CALL_PARENTS = 1000;
|
|
9192
|
+
/** Records the Agent/Task call that started the subagent making `toolCallId`. */
|
|
9193
|
+
function recordToolCallParent(session, toolCallId, parentToolUseId) {
|
|
9194
|
+
const parents = (session.toolCallParents ??= new Map());
|
|
9195
|
+
parents.delete(toolCallId);
|
|
9196
|
+
parents.set(toolCallId, parentToolUseId);
|
|
9197
|
+
if (parents.size > MAX_TOOL_CALL_PARENTS) {
|
|
9198
|
+
const oldest = parents.keys().next().value;
|
|
9199
|
+
if (oldest !== undefined)
|
|
9200
|
+
parents.delete(oldest);
|
|
9201
|
+
}
|
|
9202
|
+
}
|
|
8875
9203
|
const MAX_DISPATCHED_TOOL_CALLS = 1000;
|
|
8876
9204
|
/** Records a tool call that Claude Code sent to the tool runner. */
|
|
8877
9205
|
function recordDispatchedToolCall(session, toolCallId) {
|
|
@@ -8984,6 +9312,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8984
9312
|
let update = null;
|
|
8985
9313
|
// The tool call that this chunk finishes, if it is a tool result.
|
|
8986
9314
|
let finishedToolCallId;
|
|
9315
|
+
// The plan that this chunk approves, if it is an ExitPlanMode result.
|
|
9316
|
+
let approvedPlanUpdate;
|
|
8987
9317
|
switch (chunk.type) {
|
|
8988
9318
|
case "text":
|
|
8989
9319
|
case "text_delta": {
|
|
@@ -9141,7 +9471,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9141
9471
|
update: {
|
|
9142
9472
|
toolCallId: chunk.tool_use_id,
|
|
9143
9473
|
sessionUpdate: "tool_call_update",
|
|
9144
|
-
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution),
|
|
9474
|
+
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution, { cancelled: options?.cancelled }),
|
|
9145
9475
|
rawOutput: chunk.content,
|
|
9146
9476
|
},
|
|
9147
9477
|
});
|
|
@@ -9170,7 +9500,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9170
9500
|
},
|
|
9171
9501
|
toolCallId: chunk.tool_use_id,
|
|
9172
9502
|
sessionUpdate: "tool_call_update",
|
|
9173
|
-
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution),
|
|
9503
|
+
status: renderer.resultStatus("is_error" in chunk && chunk.is_error === true, nonExecution, { cancelled: options?.cancelled }),
|
|
9174
9504
|
rawOutput: chunk.content,
|
|
9175
9505
|
},
|
|
9176
9506
|
});
|
|
@@ -9222,6 +9552,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9222
9552
|
.result(toolUse, chunk, {
|
|
9223
9553
|
structured: toolUseResult,
|
|
9224
9554
|
nonExecution,
|
|
9555
|
+
cancelled: options?.cancelled,
|
|
9225
9556
|
})
|
|
9226
9557
|
.reverse();
|
|
9227
9558
|
for (const outputUpdate of rest.reverse()) {
|
|
@@ -9230,6 +9561,19 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9230
9561
|
}
|
|
9231
9562
|
output.push({ sessionId, update: outputUpdate });
|
|
9232
9563
|
}
|
|
9564
|
+
// An approved plan of the main conversation becomes the session's
|
|
9565
|
+
// markdown plan, after the tool call's result. An approval that
|
|
9566
|
+
// clears the context ends the tool with an error result instead, and
|
|
9567
|
+
// the restart into the fresh context publishes that plan (see
|
|
9568
|
+
// ExitPlanCoordinator.restart).
|
|
9569
|
+
if (toolUse.name === "ExitPlanMode" &&
|
|
9570
|
+
options?.clientCapabilities?.plan &&
|
|
9571
|
+
!options.parentToolUseId &&
|
|
9572
|
+
!("is_error" in chunk && chunk.is_error)) {
|
|
9573
|
+
const plan = approvedPlan(toolUse.input, toolUseResult);
|
|
9574
|
+
if (plan)
|
|
9575
|
+
approvedPlanUpdate = planModeUpdate(plan);
|
|
9576
|
+
}
|
|
9233
9577
|
update = finalUpdate;
|
|
9234
9578
|
}
|
|
9235
9579
|
// The tool_use is fully resolved now — drop it so a long session doesn't
|
|
@@ -9269,6 +9613,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9269
9613
|
output.push({ sessionId, update });
|
|
9270
9614
|
}
|
|
9271
9615
|
}
|
|
9616
|
+
if (approvedPlanUpdate)
|
|
9617
|
+
output.push({ sessionId, update: approvedPlanUpdate });
|
|
9272
9618
|
if (finishedToolCallId !== undefined) {
|
|
9273
9619
|
// The PostToolUse hook can still send the final diff, so the fields
|
|
9274
9620
|
// stay tracked until its callback leaves the registry.
|