@agentclientprotocol/claude-agent-acp 0.89.2-preview.9 → 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 +50 -1
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +493 -135
- 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,6 +3060,29 @@ export class ClaudeAcpAgent {
|
|
|
3011
3060
|
}
|
|
3012
3061
|
this.syncAwaitingUser(params.sessionId, session);
|
|
3013
3062
|
};
|
|
3063
|
+
/** Ends the display terminal of a command that went to the background, once
|
|
3064
|
+
* its task ends (see `Session.backgroundTerminals`). Claude Code reports no
|
|
3065
|
+
* exit code for it, and `completed` does not mean 0: Claude Code accepts
|
|
3066
|
+
* some non-zero codes as a success, such as grep's 1. */
|
|
3067
|
+
const endBackgroundTerminal = async (taskId) => {
|
|
3068
|
+
const terminal = session.backgroundTerminals?.get(taskId);
|
|
3069
|
+
if (!terminal)
|
|
3070
|
+
return;
|
|
3071
|
+
session.backgroundTerminals.delete(taskId);
|
|
3072
|
+
await sendUpdate({
|
|
3073
|
+
sessionId: params.sessionId,
|
|
3074
|
+
update: {
|
|
3075
|
+
sessionUpdate: "tool_call_update",
|
|
3076
|
+
toolCallId: terminal.toolCallId,
|
|
3077
|
+
_meta: {
|
|
3078
|
+
...(terminal.parentToolUseId
|
|
3079
|
+
? { claudeCode: { parentToolUseId: terminal.parentToolUseId } }
|
|
3080
|
+
: {}),
|
|
3081
|
+
terminal_exit: { terminal_id: terminal.toolCallId, exit_code: null, signal: null },
|
|
3082
|
+
},
|
|
3083
|
+
},
|
|
3084
|
+
});
|
|
3085
|
+
};
|
|
3014
3086
|
/** Settles a task. Its request may still be open, and an agent without a
|
|
3015
3087
|
* record no longer blocks the foreground work, so that is checked again. */
|
|
3016
3088
|
const settleLiveTask = (taskId) => {
|
|
@@ -3087,6 +3159,92 @@ export class ClaudeAcpAgent {
|
|
|
3087
3159
|
await settleActive(outcome);
|
|
3088
3160
|
}
|
|
3089
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
|
+
};
|
|
3090
3248
|
/** At the actual turn boundary, preview its checkpoint, settle the active
|
|
3091
3249
|
* turn exactly once, disarm the force-cancel backstop, and drop it from
|
|
3092
3250
|
* the queue. Cancellation and provider failures skip checkpoint I/O. */
|
|
@@ -3112,16 +3270,7 @@ export class ClaudeAcpAgent {
|
|
|
3112
3270
|
* ended meanwhile.
|
|
3113
3271
|
*/
|
|
3114
3272
|
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
|
-
});
|
|
3273
|
+
await closeToolCall(toolCallId, renderer.unfinished({ toolCallId, ran }));
|
|
3125
3274
|
// Cancellation can arrive while we await the client update.
|
|
3126
3275
|
if (turn.settled || session.activeTurn !== turn)
|
|
3127
3276
|
return false;
|
|
@@ -3154,6 +3303,15 @@ export class ClaudeAcpAgent {
|
|
|
3154
3303
|
return;
|
|
3155
3304
|
}
|
|
3156
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
|
+
}
|
|
3157
3315
|
turn.settling = true;
|
|
3158
3316
|
turn.settlingOutcome = result;
|
|
3159
3317
|
if (reportReason === "notReported") {
|
|
@@ -4064,9 +4222,14 @@ export class ClaudeAcpAgent {
|
|
|
4064
4222
|
this.logger.log(`Ignoring task_notification for task ${message.task_id} from earlier run ${message.run_id}`);
|
|
4065
4223
|
break;
|
|
4066
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;
|
|
4067
4230
|
// The task settled — no further tool calls can originate
|
|
4068
4231
|
// from it, so its registry entry can be dropped.
|
|
4069
|
-
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 });
|
|
4070
4233
|
await asyncTasks.taskNotification({
|
|
4071
4234
|
task_id: message.task_id,
|
|
4072
4235
|
status: message.status,
|
|
@@ -4078,7 +4241,9 @@ export class ClaudeAcpAgent {
|
|
|
4078
4241
|
subagents.discardPending(message.tool_use_id);
|
|
4079
4242
|
// The subagent streams no more text under this parent.
|
|
4080
4243
|
streamedBlocksByParent.delete(message.tool_use_id);
|
|
4244
|
+
droppedStreams.delete(message.tool_use_id);
|
|
4081
4245
|
}
|
|
4246
|
+
await endBackgroundTerminal(message.task_id);
|
|
4082
4247
|
settleLiveTask(message.task_id);
|
|
4083
4248
|
break;
|
|
4084
4249
|
case "task_updated":
|
|
@@ -4103,10 +4268,13 @@ export class ClaudeAcpAgent {
|
|
|
4103
4268
|
if (message.patch.status === "completed" ||
|
|
4104
4269
|
message.patch.status === "failed" ||
|
|
4105
4270
|
message.patch.status === "killed") {
|
|
4106
|
-
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 });
|
|
4107
4272
|
const parentToolUseId = session.liveBackgroundTasks.get(message.task_id)?.parentToolUseId;
|
|
4108
|
-
if (parentToolUseId)
|
|
4273
|
+
if (parentToolUseId) {
|
|
4109
4274
|
streamedBlocksByParent.delete(parentToolUseId);
|
|
4275
|
+
droppedStreams.delete(parentToolUseId);
|
|
4276
|
+
}
|
|
4277
|
+
await endBackgroundTerminal(message.task_id);
|
|
4110
4278
|
settleLiveTask(message.task_id);
|
|
4111
4279
|
}
|
|
4112
4280
|
else if (message.patch.status === "running" ||
|
|
@@ -4930,43 +5098,14 @@ export class ClaudeAcpAgent {
|
|
|
4930
5098
|
// reset, and the synthetic-auth/system/local-command paths
|
|
4931
5099
|
// `break` early too. Block indices restart at 0 each message, so
|
|
4932
5100
|
// leftover entries would otherwise collide with this message's
|
|
4933
|
-
// blocks and re-emit (or truncate) already-streamed text.
|
|
4934
|
-
//
|
|
4935
|
-
//
|
|
4936
|
-
//
|
|
4937
|
-
//
|
|
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.
|
|
4938
5107
|
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
|
-
}
|
|
5108
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
4970
5109
|
}
|
|
4971
5110
|
if (message.parent_tool_use_id === null &&
|
|
4972
5111
|
(message.event.type === "message_start" || message.event.type === "message_delta")) {
|
|
@@ -5020,7 +5159,71 @@ export class ClaudeAcpAgent {
|
|
|
5020
5159
|
});
|
|
5021
5160
|
}
|
|
5022
5161
|
}
|
|
5023
|
-
|
|
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, {
|
|
5024
5227
|
clientCapabilities: this.clientCapabilities,
|
|
5025
5228
|
toolCallCapabilities: this.toolCallCapabilities,
|
|
5026
5229
|
cwd: session.cwd,
|
|
@@ -5153,6 +5356,13 @@ export class ClaudeAcpAgent {
|
|
|
5153
5356
|
}
|
|
5154
5357
|
}
|
|
5155
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
|
+
}
|
|
5156
5366
|
break;
|
|
5157
5367
|
}
|
|
5158
5368
|
// Synthetic assistant frames carry the CLI's local-command output.
|
|
@@ -5284,6 +5494,7 @@ export class ClaudeAcpAgent {
|
|
|
5284
5494
|
const streamed = streamedBlocksOf(message.parent_tool_use_id);
|
|
5285
5495
|
content = unstreamedRemainder(message.message.content, streamed);
|
|
5286
5496
|
streamed.length = 0;
|
|
5497
|
+
droppedStreams.delete(message.parent_tool_use_id ?? "");
|
|
5287
5498
|
}
|
|
5288
5499
|
else if (message.type === "assistant") {
|
|
5289
5500
|
// Nested text/thinking stays internal for a client that does
|
|
@@ -5296,19 +5507,41 @@ export class ClaudeAcpAgent {
|
|
|
5296
5507
|
}
|
|
5297
5508
|
const acceptedPlanToolUseId = observeExitPlanToolResults(message, content, session);
|
|
5298
5509
|
let backgroundedToolCalls = new Set();
|
|
5510
|
+
let runningTerminal;
|
|
5299
5511
|
if (message.type === "user") {
|
|
5300
5512
|
rememberResolvedToolNames(session, content);
|
|
5301
5513
|
const backgroundBashTask = backgroundBashTaskFromToolResult(content, message.tool_use_result, session.toolUseCache);
|
|
5302
5514
|
if (backgroundBashTask)
|
|
5303
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
|
+
}
|
|
5304
5532
|
await asyncTasks.toolResults(content);
|
|
5305
5533
|
backgroundedToolCalls = backgroundedBashToolCallIds(content, session.toolUseCache, asyncTasks, backgroundBashTask);
|
|
5306
5534
|
const resumedAgentId = resumedNativeSubagentId(message.tool_use_result);
|
|
5307
5535
|
if (resumedAgentId) {
|
|
5308
|
-
|
|
5309
|
-
await subagents.taskResumed(resumedAgentId, sendUpdate, sendMessageResumePrompt(session.toolUseCache, resumedAgentId, Array.isArray(content)
|
|
5536
|
+
const resume = sendMessageResume(session.toolUseCache, resumedAgentId, Array.isArray(content)
|
|
5310
5537
|
? content.flatMap((block) => block.type === "tool_result" ? [block.tool_use_id] : [])
|
|
5311
|
-
: [])
|
|
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
|
+
}
|
|
5312
5545
|
}
|
|
5313
5546
|
}
|
|
5314
5547
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
@@ -5331,7 +5564,7 @@ export class ClaudeAcpAgent {
|
|
|
5331
5564
|
// filtered out of `content` above; blocks that do pass through
|
|
5332
5565
|
// (e.g. a subagent image) carry the stamped parentToolUseId
|
|
5333
5566
|
// meta and are excluded there.
|
|
5334
|
-
await sendUpdate(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled));
|
|
5567
|
+
await sendUpdate(withRunningTerminal(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled), runningTerminal));
|
|
5335
5568
|
}
|
|
5336
5569
|
break;
|
|
5337
5570
|
}
|
|
@@ -5355,10 +5588,14 @@ export class ClaudeAcpAgent {
|
|
|
5355
5588
|
if (toolCallId === null || !session.emittedToolCalls.has(toolCallId)) {
|
|
5356
5589
|
break;
|
|
5357
5590
|
}
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
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
|
|
5362
5599
|
: undefined;
|
|
5363
5600
|
// `tool_name` names the tool that reports the beat. When the beat
|
|
5364
5601
|
// falls back to the parent call, AIR gets the name of that call,
|
|
@@ -5581,17 +5818,29 @@ export class ClaudeAcpAgent {
|
|
|
5581
5818
|
// their compaction is running. Close it before ANY cancelled prompt
|
|
5582
5819
|
// resolves, not just the active-turn idle/backstop paths.
|
|
5583
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.
|
|
5584
5831
|
try {
|
|
5585
|
-
await session.nativeSubagentRuntime?.
|
|
5832
|
+
await session.nativeSubagentRuntime?.parentCancelled(session.nativeSubagentDeliver ??
|
|
5586
5833
|
(async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification))));
|
|
5587
5834
|
}
|
|
5588
5835
|
catch (error) {
|
|
5589
5836
|
this.logger.error(`Session ${params.sessionId}: failed to publish cancelled subagent state`, error);
|
|
5590
5837
|
}
|
|
5591
5838
|
finally {
|
|
5592
|
-
session.
|
|
5593
|
-
|
|
5594
|
-
|
|
5839
|
+
if (session.nativeSubagentRuntime?.form !== "rfd") {
|
|
5840
|
+
session.eagerToolCallSessions?.clear();
|
|
5841
|
+
session.toolCallFields?.clear();
|
|
5842
|
+
clearHookCallbacks(params.sessionId);
|
|
5843
|
+
}
|
|
5595
5844
|
}
|
|
5596
5845
|
// The user messages this cancel abandons while Claude Code may still have
|
|
5597
5846
|
// them queued: they are dropped from its queue before the interrupt.
|
|
@@ -5705,7 +5954,8 @@ export class ClaudeAcpAgent {
|
|
|
5705
5954
|
// permission request) is left to that cycle's interrupted result, like any
|
|
5706
5955
|
// live turn, or to the idle when no result comes. The captured outcome
|
|
5707
5956
|
// preserves usage and metadata (issue #844), while reporter state makes a
|
|
5708
|
-
// 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.
|
|
5709
5959
|
// Since CLI 2.1.270, Claude Code reports `running` for as long as
|
|
5710
5960
|
// background agents live, so the session state cannot tell a live
|
|
5711
5961
|
// followup from a hold. With request markers, a followup is live when one
|
|
@@ -5718,7 +5968,10 @@ export class ClaudeAcpAgent {
|
|
|
5718
5968
|
if (active &&
|
|
5719
5969
|
(active.settling || (isHeldOpen(active) && !followupLive)) &&
|
|
5720
5970
|
(active.deferredSettle ?? active.settlingOutcome)) {
|
|
5971
|
+
const closingToolCalls = session.closeCancelledToolCalls?.(active);
|
|
5721
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.
|
|
5722
5975
|
active.settled = true;
|
|
5723
5976
|
// Mirror settleActive's invariants (it is consumer-scoped and
|
|
5724
5977
|
// unreachable from here): disarm the backstop — none should be
|
|
@@ -5750,7 +6003,15 @@ export class ClaudeAcpAgent {
|
|
|
5750
6003
|
if (session.lastSessionState !== "idle") {
|
|
5751
6004
|
session.owedTrailingIdles++;
|
|
5752
6005
|
}
|
|
5753
|
-
|
|
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
|
+
}
|
|
5754
6015
|
}
|
|
5755
6016
|
}
|
|
5756
6017
|
// A steered active turn (see Turn.steeredEchoes) settles "cancelled" in the
|
|
@@ -6053,7 +6314,12 @@ export class ClaudeAcpAgent {
|
|
|
6053
6314
|
// A replay rebuilds the client view, so its plan goes out again.
|
|
6054
6315
|
if (session?.taskState)
|
|
6055
6316
|
forgetPublishedTaskPlan(session.taskState);
|
|
6056
|
-
|
|
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));
|
|
6057
6323
|
const supportsTypedFailures = supportsAirSessionFailures(this.clientCapabilities);
|
|
6058
6324
|
const activeUsageLimit = supportsTypedFailures ? activeUsageLimitMessage(messages) : undefined;
|
|
6059
6325
|
const sessionFailures = session && supportsTypedFailures
|
|
@@ -6072,7 +6338,11 @@ export class ClaudeAcpAgent {
|
|
|
6072
6338
|
})
|
|
6073
6339
|
: undefined;
|
|
6074
6340
|
let replayTurnId;
|
|
6075
|
-
|
|
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";
|
|
6076
6346
|
const replayCompactionUpdates = clientSupportsCompactionUpdates(this.clientCapabilities);
|
|
6077
6347
|
const replayTerminalStates = new Map();
|
|
6078
6348
|
const replayChildren = new Map();
|
|
@@ -6175,8 +6445,17 @@ export class ClaudeAcpAgent {
|
|
|
6175
6445
|
// Only a history with an Agent or Task launch has subagent transcripts to read.
|
|
6176
6446
|
const subagents = nativeReplayEnabled && replayChildren.size > 0
|
|
6177
6447
|
? await subagentHistory(sessionId)
|
|
6178
|
-
: {
|
|
6448
|
+
: { agents: new Map() };
|
|
6179
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);
|
|
6180
6459
|
// History persists no task_started frame, so a background task gets its
|
|
6181
6460
|
// spawn from the notification of its stop, with the tool call that
|
|
6182
6461
|
// started it. A task still running at the end of the history gets no
|
|
@@ -6195,6 +6474,8 @@ export class ClaudeAcpAgent {
|
|
|
6195
6474
|
});
|
|
6196
6475
|
// The replay counterpart of the live `task_notification` frame.
|
|
6197
6476
|
const restoreTaskNotification = async (notification) => {
|
|
6477
|
+
if (rfdReplay?.taskNotification(notification.task_id, notification.status))
|
|
6478
|
+
return;
|
|
6198
6479
|
const toolUseId = notification.tool_use_id;
|
|
6199
6480
|
if (!toolUseId)
|
|
6200
6481
|
return;
|
|
@@ -6221,7 +6502,8 @@ export class ClaudeAcpAgent {
|
|
|
6221
6502
|
});
|
|
6222
6503
|
await replayAsyncTasks.taskNotification(notification);
|
|
6223
6504
|
};
|
|
6224
|
-
|
|
6505
|
+
/** Replays `message`, from the transcript of `child` when it is set (the RFD form). */
|
|
6506
|
+
const replayMessage = async (message, child) => {
|
|
6225
6507
|
// A long history is sent in order, but with event-loop turns in between,
|
|
6226
6508
|
// so other sessions and incoming requests are not starved meanwhile.
|
|
6227
6509
|
const pause = messageLoopYielder.maybeYield();
|
|
@@ -6230,6 +6512,10 @@ export class ClaudeAcpAgent {
|
|
|
6230
6512
|
if (pending?.stopped || isReplayHiddenMetaMessage(message)) {
|
|
6231
6513
|
return;
|
|
6232
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;
|
|
6233
6519
|
if (message.type === "user" &&
|
|
6234
6520
|
message.parent_tool_use_id === null &&
|
|
6235
6521
|
typeof message.uuid === "string" &&
|
|
@@ -6269,11 +6555,14 @@ export class ClaudeAcpAgent {
|
|
|
6269
6555
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
6270
6556
|
let content = message.message.content;
|
|
6271
6557
|
const parentToolUseId = parentToolUseIdOf(message);
|
|
6272
|
-
const replayTargetSessionId =
|
|
6273
|
-
?
|
|
6274
|
-
:
|
|
6558
|
+
const replayTargetSessionId = child
|
|
6559
|
+
? child.sessionId
|
|
6560
|
+
: nativeReplayEnabled && parentToolUseId
|
|
6561
|
+
? await announceReplayChild(parentToolUseId)
|
|
6562
|
+
: sessionId;
|
|
6275
6563
|
if (message.type === "assistant" &&
|
|
6276
6564
|
parentToolUseId &&
|
|
6565
|
+
!child &&
|
|
6277
6566
|
!nativeReplayEnabled &&
|
|
6278
6567
|
!forwardSubagentText) {
|
|
6279
6568
|
content = stripSubagentTextAndThinking(content);
|
|
@@ -6287,6 +6576,9 @@ export class ClaudeAcpAgent {
|
|
|
6287
6576
|
}
|
|
6288
6577
|
// Live, the prompt loop skips this record and the SDK frame reports the stop.
|
|
6289
6578
|
content = isTaskNotificationRecord(message) ? null : stripLocalCommandMetadata(content);
|
|
6579
|
+
if (content !== null && child && rfdReplay) {
|
|
6580
|
+
content = await rfdReplay.childContent(child, message, content);
|
|
6581
|
+
}
|
|
6290
6582
|
if (content === null)
|
|
6291
6583
|
return;
|
|
6292
6584
|
}
|
|
@@ -6358,32 +6650,37 @@ export class ClaudeAcpAgent {
|
|
|
6358
6650
|
if (nativeReplayEnabled && message.type === "assistant") {
|
|
6359
6651
|
for (const toolUseId of subagentLaunchIds(content)) {
|
|
6360
6652
|
await announceReplayChild(toolUseId);
|
|
6361
|
-
const agentId = subagents.
|
|
6653
|
+
const agentId = subagents.agents.get(toolUseId)?.agentId;
|
|
6362
6654
|
if (!agentId || replayedSubagents.has(agentId))
|
|
6363
6655
|
continue;
|
|
6364
6656
|
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
|
-
}
|
|
6657
|
+
const childMessages = await readSubagentMessages(sessionId, agentId, subagents, this.logger);
|
|
6379
6658
|
registerLaunches(childMessages);
|
|
6380
6659
|
for (const childMessage of childMessages)
|
|
6381
6660
|
await replayMessage(childMessage);
|
|
6382
6661
|
}
|
|
6383
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
|
+
}
|
|
6384
6678
|
};
|
|
6385
6679
|
for (const message of messages)
|
|
6386
6680
|
await replayMessage(message);
|
|
6681
|
+
if (rfdReplay && !pending?.stopped) {
|
|
6682
|
+
await rfdReplay.finish(clientSupportsNotices(this.clientCapabilities), sessionId);
|
|
6683
|
+
}
|
|
6387
6684
|
if (nativeReplayEnabled && !pending?.stopped) {
|
|
6388
6685
|
// Claude history persists sidechain messages and Agent/Task tool uses,
|
|
6389
6686
|
// but not task_started/task_updated lifecycle frames. Recover terminal
|
|
@@ -6418,6 +6715,16 @@ export class ClaudeAcpAgent {
|
|
|
6418
6715
|
const response = await this.client.writeTextFile(params);
|
|
6419
6716
|
return response;
|
|
6420
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
|
+
}
|
|
6421
6728
|
/** Mark a client request of the subagent `agentID` (absent for the main
|
|
6422
6729
|
* conversation) as awaiting user input for exactly the lifetime of its
|
|
6423
6730
|
* promise (see `Session.openUserInputs`). Steering consults the open
|
|
@@ -6512,6 +6819,13 @@ export class ClaudeAcpAgent {
|
|
|
6512
6819
|
return;
|
|
6513
6820
|
this.startForeground(sessionId, AUTONOMOUS_CYCLE);
|
|
6514
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
|
+
}
|
|
6515
6829
|
/** End the session's model cycle that Claude Code started on its own, if one
|
|
6516
6830
|
* runs. */
|
|
6517
6831
|
endAutonomousCycle(sessionId, end) {
|
|
@@ -6541,7 +6855,7 @@ export class ClaudeAcpAgent {
|
|
|
6541
6855
|
// The request goes to a subagent's child session for a tool call of that
|
|
6542
6856
|
// subagent, but it blocks the turn of the owner session.
|
|
6543
6857
|
try {
|
|
6544
|
-
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));
|
|
6545
6859
|
}
|
|
6546
6860
|
catch (error) {
|
|
6547
6861
|
if (signal.aborted) {
|
|
@@ -6581,6 +6895,8 @@ export class ClaudeAcpAgent {
|
|
|
6581
6895
|
return;
|
|
6582
6896
|
}
|
|
6583
6897
|
session.emittedToolCalls.add(toolCallId);
|
|
6898
|
+
if (parentToolUseId)
|
|
6899
|
+
recordToolCallParent(session, toolCallId, parentToolUseId);
|
|
6584
6900
|
// SandboxNetworkAccess is a permission-only callback, not an executing
|
|
6585
6901
|
// tool: the SDK does not send a tool_result for its synthetic id.
|
|
6586
6902
|
if (!parentToolUseId &&
|
|
@@ -6605,6 +6921,7 @@ export class ClaudeAcpAgent {
|
|
|
6605
6921
|
session.emittedToolCalls.delete(toolCallId);
|
|
6606
6922
|
session.eagerToolCallSessions?.delete(toolCallId);
|
|
6607
6923
|
session.toolCallFields?.delete(toolCallId);
|
|
6924
|
+
session.toolCallParents?.delete(toolCallId);
|
|
6608
6925
|
throw error;
|
|
6609
6926
|
}
|
|
6610
6927
|
}
|
|
@@ -6879,7 +7196,7 @@ export class ClaudeAcpAgent {
|
|
|
6879
7196
|
const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
6880
7197
|
let response;
|
|
6881
7198
|
try {
|
|
6882
|
-
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));
|
|
6883
7200
|
}
|
|
6884
7201
|
catch (error) {
|
|
6885
7202
|
// A cancellation we requested (signal aborted) settles as an aborted tool
|
|
@@ -7750,6 +8067,8 @@ export class ClaudeAcpAgent {
|
|
|
7750
8067
|
prompt: input,
|
|
7751
8068
|
options,
|
|
7752
8069
|
});
|
|
8070
|
+
this.useAuthStatusSettings(settingsManager, configuredSettingsObject, userProvidedOptions?.env);
|
|
8071
|
+
const authSettingsEpoch = this.authEpoch;
|
|
7753
8072
|
timing.phase("prepare-query");
|
|
7754
8073
|
// `query()` spawns the CLI at once. Any throw between here and the
|
|
7755
8074
|
// registration in `this.sessions` would leave that child process running
|
|
@@ -7772,7 +8091,9 @@ export class ClaudeAcpAgent {
|
|
|
7772
8091
|
// Publish the identity BEFORE the guard can refuse this session. A
|
|
7773
8092
|
// refusal is exactly when the client most needs to know which account it
|
|
7774
8093
|
// was refused for.
|
|
7775
|
-
this.
|
|
8094
|
+
if (authSettingsEpoch === this.authEpoch) {
|
|
8095
|
+
this.publishSessionAccountIdentity(initializationResult.account, settingsManager.getApiKeyHelper(configuredSettingsObject));
|
|
8096
|
+
}
|
|
7776
8097
|
// Shared with the per-turn guard, so "warn once" spans the whole session.
|
|
7777
8098
|
const claudeSubscriptionGuard = {};
|
|
7778
8099
|
if (this.claudeSubscriptionGuardActive()) {
|
|
@@ -7974,7 +8295,7 @@ export class ClaudeAcpAgent {
|
|
|
7974
8295
|
messageIdToUuid: creationOpts.replayState?.messageIdToUuid ?? new Map(),
|
|
7975
8296
|
sessionFailureState: creationOpts.replayState?.sessionFailureState ?? createSessionFailureState(),
|
|
7976
8297
|
claudeSubscriptionGuard,
|
|
7977
|
-
accountKind: fromAccountInfo(initializationResult.account)?.kind,
|
|
8298
|
+
accountKind: fromAccountInfo(initializationResult.account, settingsManager.getApiKeyHelper(configuredSettingsObject))?.kind,
|
|
7978
8299
|
fileChangeReporter,
|
|
7979
8300
|
};
|
|
7980
8301
|
timing.phase("register");
|
|
@@ -8126,27 +8447,34 @@ function cancelledOutcome(session, turn) {
|
|
|
8126
8447
|
}
|
|
8127
8448
|
/** Whether the model request that `status` (a `status: requesting` frame)
|
|
8128
8449
|
* 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;
|
|
8450
|
+
* - a turn in {@link Session.cancelledPrompts} is unsettled: the stopped
|
|
8451
|
+
* turn, whose result is still to come;
|
|
8133
8452
|
* - the orphan accounting still expects the result of an abandoned prompt
|
|
8134
8453
|
* that Claude Code may still run, which it can fold into its own
|
|
8135
8454
|
* follow-up after that follow-up's first request;
|
|
8136
8455
|
* - the request serves one of {@link Session.cancelledPrompts}, as an
|
|
8137
8456
|
* abandoned prompt's request does even when the interrupt receipt left it
|
|
8138
|
-
* 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. */
|
|
8139
8464
|
function servesCancelledWork(session, status) {
|
|
8140
8465
|
const cancelled = session.cancelledPrompts;
|
|
8141
8466
|
if (!cancelled)
|
|
8142
8467
|
return false;
|
|
8143
|
-
|
|
8468
|
+
const unsettled = (session.turnQueue ?? []).filter((turn) => !turn.settled);
|
|
8469
|
+
if (unsettled.some((turn) => cancelled.has(turn.promptUuid)))
|
|
8144
8470
|
return true;
|
|
8145
8471
|
if ((session.pendingOrphanResults ?? 0) > 0 || (session.orphanCommands?.size ?? 0) > 0) {
|
|
8146
8472
|
return true;
|
|
8147
8473
|
}
|
|
8148
8474
|
const served = status.user_message_uuids;
|
|
8149
|
-
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))));
|
|
8150
8478
|
}
|
|
8151
8479
|
/** `_meta.quota` for a prompt response: what the turn spent, shaped like
|
|
8152
8480
|
* codex-acp's (snake_case container keys, camelCase counters) so a client
|
|
@@ -8872,6 +9200,18 @@ function isTaskTool(toolName) {
|
|
|
8872
9200
|
function shouldEmitToolCall(toolName) {
|
|
8873
9201
|
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
|
8874
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
|
+
}
|
|
8875
9215
|
const MAX_DISPATCHED_TOOL_CALLS = 1000;
|
|
8876
9216
|
/** Records a tool call that Claude Code sent to the tool runner. */
|
|
8877
9217
|
function recordDispatchedToolCall(session, toolCallId) {
|
|
@@ -8984,6 +9324,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8984
9324
|
let update = null;
|
|
8985
9325
|
// The tool call that this chunk finishes, if it is a tool result.
|
|
8986
9326
|
let finishedToolCallId;
|
|
9327
|
+
// The plan that this chunk approves, if it is an ExitPlanMode result.
|
|
9328
|
+
let approvedPlanUpdate;
|
|
8987
9329
|
switch (chunk.type) {
|
|
8988
9330
|
case "text":
|
|
8989
9331
|
case "text_delta": {
|
|
@@ -9141,7 +9483,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9141
9483
|
update: {
|
|
9142
9484
|
toolCallId: chunk.tool_use_id,
|
|
9143
9485
|
sessionUpdate: "tool_call_update",
|
|
9144
|
-
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 }),
|
|
9145
9487
|
rawOutput: chunk.content,
|
|
9146
9488
|
},
|
|
9147
9489
|
});
|
|
@@ -9170,7 +9512,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9170
9512
|
},
|
|
9171
9513
|
toolCallId: chunk.tool_use_id,
|
|
9172
9514
|
sessionUpdate: "tool_call_update",
|
|
9173
|
-
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 }),
|
|
9174
9516
|
rawOutput: chunk.content,
|
|
9175
9517
|
},
|
|
9176
9518
|
});
|
|
@@ -9222,6 +9564,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9222
9564
|
.result(toolUse, chunk, {
|
|
9223
9565
|
structured: toolUseResult,
|
|
9224
9566
|
nonExecution,
|
|
9567
|
+
cancelled: options?.cancelled,
|
|
9225
9568
|
})
|
|
9226
9569
|
.reverse();
|
|
9227
9570
|
for (const outputUpdate of rest.reverse()) {
|
|
@@ -9230,6 +9573,19 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9230
9573
|
}
|
|
9231
9574
|
output.push({ sessionId, update: outputUpdate });
|
|
9232
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
|
+
}
|
|
9233
9589
|
update = finalUpdate;
|
|
9234
9590
|
}
|
|
9235
9591
|
// The tool_use is fully resolved now — drop it so a long session doesn't
|
|
@@ -9269,6 +9625,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9269
9625
|
output.push({ sessionId, update });
|
|
9270
9626
|
}
|
|
9271
9627
|
}
|
|
9628
|
+
if (approvedPlanUpdate)
|
|
9629
|
+
output.push({ sessionId, update: approvedPlanUpdate });
|
|
9272
9630
|
if (finishedToolCallId !== undefined) {
|
|
9273
9631
|
// The PostToolUse hook can still send the final diff, so the fields
|
|
9274
9632
|
// stay tracked until its callback leaves the registry.
|