@agentclientprotocol/claude-agent-acp 0.85.2-preview.9 → 0.86.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/dist/acp-agent.d.ts +67 -21
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +264 -68
- package/dist/async-tasks.d.ts +52 -3
- package/dist/async-tasks.d.ts.map +1 -1
- package/dist/async-tasks.js +185 -42
- package/dist/clear-context-coordinator.d.ts +1 -0
- package/dist/clear-context-coordinator.d.ts.map +1 -1
- package/dist/clear-context-coordinator.js +2 -1
- package/dist/diff.d.ts +45 -8
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +111 -30
- package/dist/native-subagents.d.ts +7 -4
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +22 -5
- package/dist/permissions/presentation.d.ts +17 -2
- package/dist/permissions/presentation.d.ts.map +1 -1
- package/dist/permissions/presentation.js +4 -0
- package/dist/session-config-ids.d.ts +1 -0
- package/dist/session-config-ids.d.ts.map +1 -1
- package/dist/session-config-ids.js +1 -0
- package/dist/tool-calls/background.d.ts +5 -2
- package/dist/tool-calls/background.d.ts.map +1 -1
- package/dist/tool-calls/background.js +6 -11
- package/dist/tool-calls/client-capabilities.d.ts +20 -2
- package/dist/tool-calls/client-capabilities.d.ts.map +1 -1
- package/dist/tool-calls/client-capabilities.js +14 -4
- package/dist/tool-calls/facts.d.ts +8 -1
- package/dist/tool-calls/facts.d.ts.map +1 -1
- package/dist/tool-calls/renderer.d.ts +5 -1
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +14 -3
- package/dist/tool-calls/reporters/bash.d.ts.map +1 -1
- package/dist/tool-calls/reporters/bash.js +32 -5
- package/dist/tool-calls/reporters/file-edit.d.ts +1 -1
- package/dist/tool-calls/reporters/file-edit.d.ts.map +1 -1
- package/dist/tool-calls/reporters/file-edit.js +20 -14
- package/dist/v2/agent.d.ts +2 -0
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +109 -14
- package/dist/v2/initialize.d.ts +2 -5
- package/dist/v2/initialize.d.ts.map +1 -1
- package/dist/v2/initialize.js +42 -11
- package/dist/v2/permission.d.ts +25 -0
- package/dist/v2/permission.d.ts.map +1 -0
- package/dist/v2/permission.js +38 -0
- package/dist/v2/session-update.d.ts +6 -0
- package/dist/v2/session-update.d.ts.map +1 -1
- package/dist/v2/session-update.js +19 -0
- package/dist/v2/session.d.ts +29 -4
- package/dist/v2/session.d.ts.map +1 -1
- package/dist/v2/session.js +41 -6
- package/dist/v2/terminal.d.ts +42 -0
- package/dist/v2/terminal.d.ts.map +1 -0
- package/dist/v2/terminal.js +95 -0
- package/dist/v2/tool-call.d.ts +38 -0
- package/dist/v2/tool-call.d.ts.map +1 -0
- package/dist/v2/tool-call.js +49 -0
- package/package.json +1 -1
package/dist/acp-agent.js
CHANGED
|
@@ -5,7 +5,7 @@ import { sanitizeTitle, SessionTitles } from "./session-titles.js";
|
|
|
5
5
|
import { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, asSdkSessionNotification, clientSupportsSubagents, } from "./acp-subagents.js";
|
|
6
6
|
import { isNativeSubagentControlTool, isNativeSubagentControlUpdate, NativeSubagentRuntime, nativeSubagentState, resumedNativeSubagentId, sendMessageResumePrompt, } from "./native-subagents.js";
|
|
7
7
|
import { AIR_ASYNC_TASKS_CAPABILITY, 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
|
-
import { AsyncTaskRuntime, backgroundBashTaskFromToolResult, clientSupportsAsyncTasks, } from "./async-tasks.js";
|
|
8
|
+
import { AsyncTaskRuntime, backgroundBashTaskFromToolResult, backgroundedBashToolCallIds, clientSupportsAsyncTasks, } from "./async-tasks.js";
|
|
9
9
|
import { AUTH_STATUS_PROBE_TIMEOUT_MS, AUTH_STATUS_UPDATE_METHOD, authStatusCapability, fromAccountInfo, fromCliStatus, gatewayAuthStatus, mergeAuthStatus, notLoggedInAuthStatus, sameAuthStatus, } from "./auth-status.js";
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
@@ -21,7 +21,7 @@ import { SessionTiming } from "./session-timing.js";
|
|
|
21
21
|
import { ALLOW_BYPASS } from "./permissions/modes.js";
|
|
22
22
|
import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
|
|
23
23
|
import { buildClaudePermissionOptions } from "./permissions/options.js";
|
|
24
|
-
import { buildClaudePermissionPresentation } from "./permissions/presentation.js";
|
|
24
|
+
import { buildClaudePermissionPresentation, } from "./permissions/presentation.js";
|
|
25
25
|
import { decodeClaudePermissionResponse } from "./permissions/response.js";
|
|
26
26
|
import { SettingsManager } from "./settings.js";
|
|
27
27
|
import { activeUsageLimitMessage, airSessionFailureCapabilityMeta, assistantMessageText, createSessionFailureState, isSyntheticUsageLimitMessage, providerFailureCategory, SessionFailureController, sessionFailureMeta, supportsAirSessionFailures, } from "./session-failure-extension.js";
|
|
@@ -39,8 +39,10 @@ import { AcpToolCallRenderer } from "./tool-calls/renderer.js";
|
|
|
39
39
|
import { resolveSkillPath } from "./tool-calls/reporters/interaction.js";
|
|
40
40
|
import { nodeToWebReadable, nodeToWebWritable, Pushable, raceTimeoutAndAbort, unreachable, } from "./utils.js";
|
|
41
41
|
import { acceptedPlanToolResult, ExitPlanCoordinator, isExitPlanInterruptionResult, observeExitPlanToolResults, } from "./exit-plan.js";
|
|
42
|
+
import { DEFAULT_AGENT_ID } from "./session-config-ids.js";
|
|
42
43
|
import { parseToolResultMeta } from "./tool-result-meta.js";
|
|
43
44
|
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./usage-markdown.js";
|
|
45
|
+
export { DEFAULT_AGENT_ID } from "./session-config-ids.js";
|
|
44
46
|
import { MCP_AVAILABLE_COMMAND, parseMcpCommand, runMcpCommand } from "./mcp-command.js";
|
|
45
47
|
import { MODE_CONFIG_ID, resolveInitialPermissionMode, SessionModeManager, } from "./session-mode.js";
|
|
46
48
|
import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableModels, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
@@ -79,6 +81,10 @@ const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
|
79
81
|
* pre-empt a slow-but-healthy interrupt. */
|
|
80
82
|
const DEFAULT_FORCE_CANCEL_GRACE_MS = 30_000;
|
|
81
83
|
const STRUCTURED_USAGE_TIMEOUT_MS = 5_000;
|
|
84
|
+
/** How long a session teardown waits for the turns that it cancels to end.
|
|
85
|
+
* The teardown wakes the consumer itself, so they end within a few ticks
|
|
86
|
+
* unless the consumer is wedged. */
|
|
87
|
+
const TEARDOWN_TURN_END_TIMEOUT_MS = 5_000;
|
|
82
88
|
/** The number of settled subagents whose parent tool call a session keeps for
|
|
83
89
|
* a later resume (see `resumableSubagents`). */
|
|
84
90
|
const MAX_RESUMABLE_SUBAGENTS = 256;
|
|
@@ -1151,7 +1157,14 @@ export class ClaudeAcpAgent {
|
|
|
1151
1157
|
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
1152
1158
|
* tests can shrink it. */
|
|
1153
1159
|
forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
|
|
1154
|
-
|
|
1160
|
+
/**
|
|
1161
|
+
* The connection serves ACP v2 (`src/v2/`). The agent still speaks v1
|
|
1162
|
+
* types, except for the tool call content that v2 needs and v1 cannot
|
|
1163
|
+
* express: see {@link ToolCallClientCapabilities.v2}.
|
|
1164
|
+
*/
|
|
1165
|
+
v2;
|
|
1166
|
+
constructor(client, logger, options = {}) {
|
|
1167
|
+
this.v2 = options.v2 ?? false;
|
|
1155
1168
|
this.sessions = {};
|
|
1156
1169
|
this.client = new ChangedMetaClient(client, () => this.toolCallCapabilities.air.client);
|
|
1157
1170
|
this.logger = logger ?? console;
|
|
@@ -1208,7 +1221,9 @@ export class ClaudeAcpAgent {
|
|
|
1208
1221
|
}
|
|
1209
1222
|
async initialize(request) {
|
|
1210
1223
|
this.clientCapabilities = request.clientCapabilities;
|
|
1211
|
-
this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities
|
|
1224
|
+
this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities, {
|
|
1225
|
+
v2: this.v2,
|
|
1226
|
+
});
|
|
1212
1227
|
// Learn the auth identity in the background: `initialize` never waits on
|
|
1213
1228
|
// the CLI probe, and no snapshot rides in its response. When the probe
|
|
1214
1229
|
// lands it calls `setAuthStatus`, which pushes `_auth/status_update` — the
|
|
@@ -2450,6 +2465,7 @@ export class ClaudeAcpAgent {
|
|
|
2450
2465
|
// A task that a subagent tool call started belongs to the child
|
|
2451
2466
|
// session of that tool call, like the tool call itself.
|
|
2452
2467
|
routeOf: (toolCallId) => session.nativeSubagentRuntime?.routeOfToolCall(toolCallId, session.eagerToolCallSessions?.get(toolCallId)),
|
|
2468
|
+
toolNameOf: (toolCallId) => session.toolUseCache[toolCallId]?.name ?? session.resolvedToolNames?.get(toolCallId),
|
|
2453
2469
|
}));
|
|
2454
2470
|
const compaction = new ContextCompactionLifecycle((notification) => sendUpdate(notification), {
|
|
2455
2471
|
sessionId: params.sessionId,
|
|
@@ -2510,6 +2526,7 @@ export class ClaudeAcpAgent {
|
|
|
2510
2526
|
update.sessionUpdate === "tool_call_update" &&
|
|
2511
2527
|
(update.status === "completed" || update.status === "failed")) {
|
|
2512
2528
|
session.eagerToolCallSessions?.delete(toolCallId);
|
|
2529
|
+
session.dispatchedToolCalls?.delete(toolCallId);
|
|
2513
2530
|
}
|
|
2514
2531
|
};
|
|
2515
2532
|
// toAcpNotifications registers deferred tool hooks that publish through
|
|
@@ -2951,38 +2968,48 @@ export class ClaudeAcpAgent {
|
|
|
2951
2968
|
// Check only this turn's tools. A tool_result removes its id from
|
|
2952
2969
|
// emittedToolCalls even when its PostToolUse hook has not arrived yet.
|
|
2953
2970
|
const unfinished = [...(turn.foregroundToolCallIds ?? [])].filter((id) => session.emittedToolCalls.has(id) && !backgroundTools.has(id));
|
|
2954
|
-
|
|
2955
|
-
|
|
2971
|
+
/** Closes a tool call as failed. Returns false when the turn ended meanwhile. */
|
|
2972
|
+
const failToolCall = async (toolCallId, text) => {
|
|
2973
|
+
// A late hook must not overwrite the failure we are about to send.
|
|
2974
|
+
unregisterHookCallback(toolCallId);
|
|
2975
|
+
session.emittedToolCalls.delete(toolCallId);
|
|
2976
|
+
delete session.toolUseCache[toolCallId];
|
|
2977
|
+
session.toolCallFields?.delete(toolCallId);
|
|
2978
|
+
session.dispatchedToolCalls?.delete(toolCallId);
|
|
2979
|
+
await sendUpdate({
|
|
2980
|
+
sessionId: params.sessionId,
|
|
2981
|
+
update: {
|
|
2982
|
+
sessionUpdate: "tool_call_update",
|
|
2983
|
+
toolCallId,
|
|
2984
|
+
status: "failed",
|
|
2985
|
+
content: [{ type: "content", content: { type: "text", text } }],
|
|
2986
|
+
},
|
|
2987
|
+
});
|
|
2988
|
+
// Cancellation can arrive while we await the client update.
|
|
2989
|
+
if (turn.settled || session.activeTurn !== turn)
|
|
2990
|
+
return false;
|
|
2991
|
+
if (session.cancelled) {
|
|
2992
|
+
await settleActive({ ...result, stopReason: "cancelled" });
|
|
2993
|
+
return false;
|
|
2994
|
+
}
|
|
2995
|
+
return true;
|
|
2996
|
+
};
|
|
2997
|
+
// A streamed tool_use that never reached a complete assistant message
|
|
2998
|
+
// never ran: Claude abandoned it, for example for a steering message.
|
|
2999
|
+
// It is not a failure of the turn.
|
|
3000
|
+
const abandoned = unfinished.filter((id) => !session.dispatchedToolCalls?.has(id));
|
|
3001
|
+
const stuck = unfinished.filter((id) => session.dispatchedToolCalls?.has(id));
|
|
3002
|
+
for (const toolCallId of abandoned) {
|
|
3003
|
+
if (!(await failToolCall(toolCallId, "Claude stopped this tool call before it ran."))) {
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
if (stuck.length > 0) {
|
|
3008
|
+
const message = `Claude ended the turn without returning results for tool calls: ${stuck.join(", ")}`;
|
|
2956
3009
|
this.logger.error(`Session ${params.sessionId}, turn ${turn.promptUuid}, stopReason=${result.stopReason}: ${message}`);
|
|
2957
3010
|
// Fail every unfinished tool before reporting one error for the prompt.
|
|
2958
|
-
for (const toolCallId of
|
|
2959
|
-
|
|
2960
|
-
unregisterHookCallback(toolCallId);
|
|
2961
|
-
session.emittedToolCalls.delete(toolCallId);
|
|
2962
|
-
delete session.toolUseCache[toolCallId];
|
|
2963
|
-
session.toolCallFields?.delete(toolCallId);
|
|
2964
|
-
await sendUpdate({
|
|
2965
|
-
sessionId: params.sessionId,
|
|
2966
|
-
update: {
|
|
2967
|
-
sessionUpdate: "tool_call_update",
|
|
2968
|
-
toolCallId,
|
|
2969
|
-
status: "failed",
|
|
2970
|
-
content: [
|
|
2971
|
-
{
|
|
2972
|
-
type: "content",
|
|
2973
|
-
content: {
|
|
2974
|
-
type: "text",
|
|
2975
|
-
text: "Claude ended the turn without returning a result for this tool.",
|
|
2976
|
-
},
|
|
2977
|
-
},
|
|
2978
|
-
],
|
|
2979
|
-
},
|
|
2980
|
-
});
|
|
2981
|
-
// Cancellation can arrive while we await the client update.
|
|
2982
|
-
if (turn.settled || session.activeTurn !== turn)
|
|
2983
|
-
return;
|
|
2984
|
-
if (session.cancelled) {
|
|
2985
|
-
await settleActive({ ...result, stopReason: "cancelled" });
|
|
3011
|
+
for (const toolCallId of stuck) {
|
|
3012
|
+
if (!(await failToolCall(toolCallId, "Claude ended the turn without returning a result for this tool."))) {
|
|
2986
3013
|
return;
|
|
2987
3014
|
}
|
|
2988
3015
|
}
|
|
@@ -3130,11 +3157,18 @@ export class ClaudeAcpAgent {
|
|
|
3130
3157
|
.then((result) => ({ kind: "message", result }));
|
|
3131
3158
|
const nextMessage = pendingNext;
|
|
3132
3159
|
// Fresh abort listener per iteration, removed when next() wins, so a
|
|
3133
|
-
// long-lived session doesn't accumulate listeners on one signal.
|
|
3160
|
+
// long-lived session doesn't accumulate listeners on one signal. An
|
|
3161
|
+
// abort that fired while the consumer was busy elsewhere, such as
|
|
3162
|
+
// sending an update, still wakes it: a listener added to an aborted
|
|
3163
|
+
// signal never fires. The consumer re-arms after each abort it
|
|
3164
|
+
// handles, so an aborted signal here is always an unhandled abort.
|
|
3134
3165
|
let onAbort;
|
|
3135
3166
|
const abortRace = new Promise((resolve) => {
|
|
3136
3167
|
onAbort = () => resolve("abort");
|
|
3137
|
-
cancelController.signal.
|
|
3168
|
+
if (cancelController.signal.aborted)
|
|
3169
|
+
onAbort();
|
|
3170
|
+
else
|
|
3171
|
+
cancelController.signal.addEventListener("abort", onAbort, { once: true });
|
|
3138
3172
|
});
|
|
3139
3173
|
const raced = await Promise.race([nextMessage, abortRace]);
|
|
3140
3174
|
cancelController.signal.removeEventListener("abort", onAbort);
|
|
@@ -4723,6 +4757,7 @@ export class ClaudeAcpAgent {
|
|
|
4723
4757
|
}
|
|
4724
4758
|
for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, this.client, this.logger, {
|
|
4725
4759
|
clientCapabilities: this.clientCapabilities,
|
|
4760
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
4726
4761
|
cwd: session.cwd,
|
|
4727
4762
|
taskState: session.taskState,
|
|
4728
4763
|
emittedToolCalls: session.emittedToolCalls,
|
|
@@ -4747,6 +4782,8 @@ export class ClaudeAcpAgent {
|
|
|
4747
4782
|
}
|
|
4748
4783
|
case "user":
|
|
4749
4784
|
case "assistant": {
|
|
4785
|
+
if (message.type === "assistant")
|
|
4786
|
+
recordDispatchedToolUses(session, message.message.content);
|
|
4750
4787
|
// Record the ACP messageId -> SDK uuid mapping for this message
|
|
4751
4788
|
// (including replays). The consolidated message carries both ids, so
|
|
4752
4789
|
// this is where we learn the uuid the SDK's rewind/resume APIs key on
|
|
@@ -4926,6 +4963,7 @@ export class ClaudeAcpAgent {
|
|
|
4926
4963
|
if (typeof stripped === "string") {
|
|
4927
4964
|
for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
4928
4965
|
clientCapabilities: this.clientCapabilities,
|
|
4966
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
4929
4967
|
parentToolUseId: message.parent_tool_use_id,
|
|
4930
4968
|
cwd: session.cwd,
|
|
4931
4969
|
taskState: session.taskState,
|
|
@@ -4992,11 +5030,14 @@ export class ClaudeAcpAgent {
|
|
|
4992
5030
|
content = message.message.content;
|
|
4993
5031
|
}
|
|
4994
5032
|
const acceptedPlanToolUseId = observeExitPlanToolResults(message, content, session);
|
|
4995
|
-
let
|
|
5033
|
+
let backgroundedToolCalls = new Set();
|
|
4996
5034
|
if (message.type === "user") {
|
|
4997
|
-
|
|
5035
|
+
rememberResolvedToolNames(session, content);
|
|
5036
|
+
const backgroundBashTask = backgroundBashTaskFromToolResult(content, message.tool_use_result, session.toolUseCache);
|
|
4998
5037
|
if (backgroundBashTask)
|
|
4999
5038
|
await asyncTasks.taskBackgrounded(backgroundBashTask);
|
|
5039
|
+
await asyncTasks.toolResults(content);
|
|
5040
|
+
backgroundedToolCalls = backgroundedBashToolCallIds(content, session.toolUseCache, asyncTasks, backgroundBashTask);
|
|
5000
5041
|
const resumedAgentId = resumedNativeSubagentId(message.tool_use_result);
|
|
5001
5042
|
if (resumedAgentId) {
|
|
5002
5043
|
resumeLiveTask(resumedAgentId);
|
|
@@ -5007,6 +5048,7 @@ export class ClaudeAcpAgent {
|
|
|
5007
5048
|
}
|
|
5008
5049
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
5009
5050
|
clientCapabilities: this.clientCapabilities,
|
|
5051
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
5010
5052
|
parentToolUseId: message.parent_tool_use_id,
|
|
5011
5053
|
cwd: session.cwd,
|
|
5012
5054
|
taskState: session.taskState,
|
|
@@ -5024,7 +5066,7 @@ export class ClaudeAcpAgent {
|
|
|
5024
5066
|
// filtered out of `content` above; blocks that do pass through
|
|
5025
5067
|
// (e.g. a subagent image) carry the stamped parentToolUseId
|
|
5026
5068
|
// meta and are excluded there.
|
|
5027
|
-
await sendUpdate(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId),
|
|
5069
|
+
await sendUpdate(backgroundedBashToolCall(acceptedPlanToolResult(notification, acceptedPlanToolUseId), backgroundedToolCalls, asyncTasks.enabled));
|
|
5028
5070
|
}
|
|
5029
5071
|
break;
|
|
5030
5072
|
}
|
|
@@ -5094,6 +5136,7 @@ export class ClaudeAcpAgent {
|
|
|
5094
5136
|
await finishLifecycle("failed", "failed", "during conversation reset");
|
|
5095
5137
|
subagents.clear();
|
|
5096
5138
|
asyncTasks.clear();
|
|
5139
|
+
session.resolvedToolNames?.clear();
|
|
5097
5140
|
session.eagerToolCallSessions?.clear();
|
|
5098
5141
|
session.toolCallFields?.clear();
|
|
5099
5142
|
clearHookCallbacks(params.sessionId);
|
|
@@ -5554,12 +5597,18 @@ export class ClaudeAcpAgent {
|
|
|
5554
5597
|
session.query.close();
|
|
5555
5598
|
}
|
|
5556
5599
|
/** Cleanly tear down a session: cancel in-flight work, release stream
|
|
5557
|
-
* resources, and remove it from the session map.
|
|
5600
|
+
* resources, and remove it from the session map. Returns once the turns it
|
|
5601
|
+
* cancels have ended, so their end (a v1 `session/prompt` response, a v2
|
|
5602
|
+
* `idle` state) goes out before the caller answers: ACP v2's
|
|
5603
|
+
* `session/close` cancels "as if `session/cancel` had been called", then
|
|
5604
|
+
* frees the session. A wedged consumer holds it at most
|
|
5605
|
+
* {@link TEARDOWN_TURN_END_TIMEOUT_MS}. */
|
|
5558
5606
|
async teardownSession(sessionId) {
|
|
5559
5607
|
const session = this.sessions[sessionId];
|
|
5560
5608
|
if (!session) {
|
|
5561
5609
|
return;
|
|
5562
5610
|
}
|
|
5611
|
+
const turnsEnded = Promise.all((session.turnQueue ?? []).flatMap((turn) => (turn.completion ? [turn.completion] : [])));
|
|
5563
5612
|
try {
|
|
5564
5613
|
await this.cancelTurns({ sessionId }, { awaitInterrupt: false });
|
|
5565
5614
|
}
|
|
@@ -5587,6 +5636,10 @@ export class ClaudeAcpAgent {
|
|
|
5587
5636
|
session.nativeSubagentRuntime?.clear();
|
|
5588
5637
|
session.asyncTaskRuntime?.clear();
|
|
5589
5638
|
delete this.sessions[sessionId];
|
|
5639
|
+
const ended = await raceTimeoutAndAbort(turnsEnded, TEARDOWN_TURN_END_TIMEOUT_MS, new AbortController().signal);
|
|
5640
|
+
if (ended.type === "timeout") {
|
|
5641
|
+
this.logger.error(`Session ${sessionId}: its cancelled turns did not end within ${TEARDOWN_TURN_END_TIMEOUT_MS}ms of the teardown`);
|
|
5642
|
+
}
|
|
5590
5643
|
}
|
|
5591
5644
|
/** Tear down all active sessions. Called when the ACP connection closes. */
|
|
5592
5645
|
async dispose() {
|
|
@@ -5843,6 +5896,7 @@ export class ClaudeAcpAgent {
|
|
|
5843
5896
|
? (notification) => ({ ...notification, sessionId: target })
|
|
5844
5897
|
: undefined;
|
|
5845
5898
|
},
|
|
5899
|
+
toolNameOf: (toolCallId) => replayToolUses.get(toolCallId)?.name,
|
|
5846
5900
|
});
|
|
5847
5901
|
// The replay counterpart of the live `task_notification` frame.
|
|
5848
5902
|
const restoreTaskNotification = async (notification) => {
|
|
@@ -5926,6 +5980,8 @@ export class ClaudeAcpAgent {
|
|
|
5926
5980
|
}
|
|
5927
5981
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
5928
5982
|
if (message.message.role === "user") {
|
|
5983
|
+
// Like live, a tool result gives the output path of a known task.
|
|
5984
|
+
await replayAsyncTasks.toolResults(content);
|
|
5929
5985
|
for (const notification of taskNotificationsOf(content)) {
|
|
5930
5986
|
await restoreTaskNotification(notification);
|
|
5931
5987
|
}
|
|
@@ -5981,6 +6037,7 @@ export class ClaudeAcpAgent {
|
|
|
5981
6037
|
registerHooks: false,
|
|
5982
6038
|
replay: true,
|
|
5983
6039
|
clientCapabilities: this.clientCapabilities,
|
|
6040
|
+
toolCallCapabilities: this.toolCallCapabilities,
|
|
5984
6041
|
cwd: replayState()?.cwd,
|
|
5985
6042
|
taskState: replayState()?.taskState,
|
|
5986
6043
|
messageId: replayMessageId,
|
|
@@ -6120,8 +6177,10 @@ export class ClaudeAcpAgent {
|
|
|
6120
6177
|
// Do not rely on every ACP client settling requestPermission after the
|
|
6121
6178
|
// cancellation signal. The local race guarantees that Claude's tool call
|
|
6122
6179
|
// is released even when an older or broken client ignores $/cancel_request.
|
|
6180
|
+
// The request goes to a subagent's child session for a tool call of that
|
|
6181
|
+
// subagent, but it blocks the turn of the owner session.
|
|
6123
6182
|
try {
|
|
6124
|
-
return await this.withPendingUserInput(
|
|
6183
|
+
return await this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal));
|
|
6125
6184
|
}
|
|
6126
6185
|
catch (error) {
|
|
6127
6186
|
if (signal.aborted) {
|
|
@@ -6154,6 +6213,8 @@ export class ClaudeAcpAgent {
|
|
|
6154
6213
|
toolCallFieldsOf(session).pinContent(toolCallId, previewContent);
|
|
6155
6214
|
}
|
|
6156
6215
|
};
|
|
6216
|
+
// A permission request comes only for a tool call that is about to run.
|
|
6217
|
+
recordDispatchedToolCall(session, toolCallId);
|
|
6157
6218
|
if (session.emittedToolCalls.has(toolCallId)) {
|
|
6158
6219
|
pinPreview();
|
|
6159
6220
|
return;
|
|
@@ -6231,7 +6292,7 @@ export class ClaudeAcpAgent {
|
|
|
6231
6292
|
// Like permission requests, the elicitation references this toolUseID, so
|
|
6232
6293
|
// make sure the tool_call has surfaced to the client before we send it.
|
|
6233
6294
|
await this.ensureToolCallEmitted(sessionId, toolName, toolUseID, toolInput, parentToolUseId, signal, permissionSessionId);
|
|
6234
|
-
return this.handleAskUserQuestion(permissionSessionId, toolInput, toolUseID, signal);
|
|
6295
|
+
return this.handleAskUserQuestion(sessionId, permissionSessionId, toolInput, toolUseID, signal);
|
|
6235
6296
|
}
|
|
6236
6297
|
// Do not auto-allow here based on the session's advertised mode. Claude
|
|
6237
6298
|
// Code applies bypassPermissions before invoking canUseTool; a request
|
|
@@ -6247,8 +6308,8 @@ export class ClaudeAcpAgent {
|
|
|
6247
6308
|
const noPersistentRule = matchedAskRule !== undefined || suppressAlwaysAllowRule === true;
|
|
6248
6309
|
const durableChangeSet = normalizeDurablePermissionChangeSet(suggestions, noPersistentRule);
|
|
6249
6310
|
const capabilities = this.toolCallCapabilities;
|
|
6250
|
-
const previewContent = capabilities.diffPatch
|
|
6251
|
-
? await previewPatchContent(toolName, toolInput, session.cwd)
|
|
6311
|
+
const previewContent = capabilities.diffPatch || capabilities.v2
|
|
6312
|
+
? await previewPatchContent(toolName, toolInput, session.cwd, capabilities.v2 ? "v2" : "air")
|
|
6252
6313
|
: undefined;
|
|
6253
6314
|
const presentation = buildClaudePermissionPresentation({
|
|
6254
6315
|
toolName,
|
|
@@ -6385,13 +6446,15 @@ export class ClaudeAcpAgent {
|
|
|
6385
6446
|
* Present the built-in AskUserQuestion tool's questions as an ACP form
|
|
6386
6447
|
* elicitation and return the answers as the tool's `updatedInput`. Called from
|
|
6387
6448
|
* `canUseTool` since that is where the SDK routes the tool's permission check.
|
|
6449
|
+
* The elicitation goes to `requestSessionId`, a subagent's child session for
|
|
6450
|
+
* a question it asks, and blocks the turn of `sessionId`.
|
|
6388
6451
|
*/
|
|
6389
|
-
async handleAskUserQuestion(sessionId, toolInput, toolUseID, signal) {
|
|
6452
|
+
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID, signal) {
|
|
6390
6453
|
const questions = extractAskUserQuestions(toolInput);
|
|
6391
6454
|
if (!questions) {
|
|
6392
6455
|
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
6393
6456
|
}
|
|
6394
|
-
const createRequest = askUserQuestionsToCreateRequest(questions,
|
|
6457
|
+
const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
6395
6458
|
let response;
|
|
6396
6459
|
try {
|
|
6397
6460
|
response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal));
|
|
@@ -6479,15 +6542,21 @@ export class ClaudeAcpAgent {
|
|
|
6479
6542
|
});
|
|
6480
6543
|
}
|
|
6481
6544
|
/**
|
|
6482
|
-
*
|
|
6545
|
+
* Refine a heuristic context window with `getContextUsage().rawMaxTokens`,
|
|
6483
6546
|
* without blocking the caller. The text heuristic misses natively-1M models
|
|
6484
|
-
* whose picker rows carry no "1m" token (`sonnet`, and since CLI
|
|
6485
|
-
* `opus`/`default`),
|
|
6486
|
-
* result's modelUsage.
|
|
6487
|
-
*
|
|
6488
|
-
*
|
|
6489
|
-
*
|
|
6490
|
-
*
|
|
6547
|
+
* whose picker rows carry no "1m" token (e.g. `sonnet`, and since CLI
|
|
6548
|
+
* 2.1.283 `opus`/`default`), so those would otherwise show 200k until the
|
|
6549
|
+
* first result's modelUsage arrives.
|
|
6550
|
+
*
|
|
6551
|
+
* Never awaited: SDK control requests are serialized, so awaiting here
|
|
6552
|
+
* would delay session/new or a model switch (~15s on older CLIs, issues
|
|
6553
|
+
* #886/#880; ~0.5s on 2.1.283). Requests the `summary` detail instead of
|
|
6554
|
+
* the default `full` breakdown, which issues one `messages/count_tokens`
|
|
6555
|
+
* call per category (~18 for a bare session) and hit rate limits on every
|
|
6556
|
+
* model switch; `summary` answers locally with the same `rawMaxTokens`.
|
|
6557
|
+
*
|
|
6558
|
+
* Does not write `contextWindowCache` (that stays keyed to the
|
|
6559
|
+
* `result.modelUsage` spellings) — a later result still overwrites this.
|
|
6491
6560
|
*/
|
|
6492
6561
|
refreshContextWindowInBackground(sessionId, session) {
|
|
6493
6562
|
if (session.contextWindowAuthoritative)
|
|
@@ -6499,7 +6568,7 @@ export class ClaudeAcpAgent {
|
|
|
6499
6568
|
session.models.currentModelId === modelId;
|
|
6500
6569
|
// A synchronous throw must not fail the caller either.
|
|
6501
6570
|
Promise.resolve()
|
|
6502
|
-
.then(() => query.getContextUsage())
|
|
6571
|
+
.then(() => query.getContextUsage({ detail: "summary" }))
|
|
6503
6572
|
.then((usage) => {
|
|
6504
6573
|
if (!stillCurrent() || session.contextWindowAuthoritative)
|
|
6505
6574
|
return;
|
|
@@ -6569,7 +6638,7 @@ export class ClaudeAcpAgent {
|
|
|
6569
6638
|
const seedEffort = effortPinnedForNewModel
|
|
6570
6639
|
? pinnedEffort
|
|
6571
6640
|
: settingsEffortForModel(mergeEffortSettings(session.settingsManager.getSettings(), session.effortSettingsOverride), newModelInfo, value);
|
|
6572
|
-
session.configOptions = buildConfigOptions(session.modes, session.models, session.modelInfos, seedEffort, {
|
|
6641
|
+
session.configOptions = buildConfigOptions(session.modes, session.models, session.modelInfos, seedEffort, session.agents, session.currentAgent, {
|
|
6573
6642
|
// The toggle follows the newly selected model: it disappears when the
|
|
6574
6643
|
// model lacks fast support and reappears (with the retained user
|
|
6575
6644
|
// intent) when a supporting model is selected again.
|
|
@@ -6648,6 +6717,18 @@ export class ClaudeAcpAgent {
|
|
|
6648
6717
|
if (modelChanged)
|
|
6649
6718
|
this.refreshContextWindowInBackground(sessionId, session);
|
|
6650
6719
|
}
|
|
6720
|
+
else if (configId === AGENT_CONFIG_ID) {
|
|
6721
|
+
// Live agent switch — no subprocess restart needed. Apply the SDK flag
|
|
6722
|
+
// first so a rejected control request leaves both `currentAgent` and the
|
|
6723
|
+
// config option untouched (no UI/SDK desync). Passing `null` clears the
|
|
6724
|
+
// flag layer back to the standard Claude Code agent; the change takes
|
|
6725
|
+
// effect on the next turn (SDK >= 0.3.161).
|
|
6726
|
+
await session.query.applyFlagSettings({
|
|
6727
|
+
agent: value === DEFAULT_AGENT_ID ? null : value,
|
|
6728
|
+
});
|
|
6729
|
+
session.currentAgent = value;
|
|
6730
|
+
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
6731
|
+
}
|
|
6651
6732
|
else if (configId === EFFORT_CONFIG_ID) {
|
|
6652
6733
|
// Apply first so a rejected control request cannot leave the displayed
|
|
6653
6734
|
// value ahead of the SDK flag layer.
|
|
@@ -6954,14 +7035,7 @@ export class ClaudeAcpAgent {
|
|
|
6954
7035
|
resumed: resumedPermissionMode,
|
|
6955
7036
|
defaultMode: settingsManager.getSettings().permissions?.defaultMode,
|
|
6956
7037
|
}, this.logger, allowBypass);
|
|
6957
|
-
const userProvidedOptions = sessionMeta?.claudeCode?.options
|
|
6958
|
-
? { ...sessionMeta.claudeCode.options }
|
|
6959
|
-
: undefined;
|
|
6960
|
-
// Main-thread agent selection is intentionally not part of this adapter's
|
|
6961
|
-
// ACP contract. Ignore the provider-specific option instead of forwarding
|
|
6962
|
-
// hidden state that the Client cannot inspect or change.
|
|
6963
|
-
if (userProvidedOptions)
|
|
6964
|
-
delete userProvidedOptions.agent;
|
|
7038
|
+
const userProvidedOptions = sessionMeta?.claudeCode?.options;
|
|
6965
7039
|
const forwardSubagentText = this.forwardsSubagentText(params._meta);
|
|
6966
7040
|
// Configure thinking behavior from environment variable
|
|
6967
7041
|
const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
|
|
@@ -7326,6 +7400,17 @@ export class ClaudeAcpAgent {
|
|
|
7326
7400
|
allowBypass,
|
|
7327
7401
|
});
|
|
7328
7402
|
timing.phase("modes");
|
|
7403
|
+
const agents = await discoverCustomAgents(q);
|
|
7404
|
+
timing.phase("agents");
|
|
7405
|
+
// Only adopt the requested agent as the selected value if it's one we
|
|
7406
|
+
// actually surface in the picker. A built-in (filtered out above) or
|
|
7407
|
+
// otherwise-unknown name would leave the config option's `currentValue`
|
|
7408
|
+
// pointing at an entry not in its own `options` list, which clients render
|
|
7409
|
+
// as a blank/invalid selection.
|
|
7410
|
+
const requestedAgent = userProvidedOptions?.agent;
|
|
7411
|
+
const currentAgent = requestedAgent && agents.some((a) => a.name === requestedAgent)
|
|
7412
|
+
? requestedAgent
|
|
7413
|
+
: DEFAULT_AGENT_ID;
|
|
7329
7414
|
// Seed Fast mode from the SDK's reported state so the UI reflects reality
|
|
7330
7415
|
// (the CLI may start a session with fast mode already on, or force it off
|
|
7331
7416
|
// when `fastModePerSessionOptIn` is set). The toggle is only surfaced while
|
|
@@ -7351,7 +7436,7 @@ export class ClaudeAcpAgent {
|
|
|
7351
7436
|
// Legacy clients still leave effort resolution entirely to the CLI.
|
|
7352
7437
|
const useRecommendedValue = clientSupportsRecommendedConfigValue(this.clientCapabilities);
|
|
7353
7438
|
const effortSettings = mergeEffortSettings(settingsManager.getSettings(), configuredSettingsObject);
|
|
7354
|
-
const configOptions = buildConfigOptions(modes, models, modelInfos, userProvidedOptions?.effort ?? settingsEffortForModel(effortSettings, currentModelInfo), fastMode, {
|
|
7439
|
+
const configOptions = buildConfigOptions(modes, models, modelInfos, userProvidedOptions?.effort ?? settingsEffortForModel(effortSettings, currentModelInfo), agents, currentAgent, fastMode, {
|
|
7355
7440
|
useRecommendedValue,
|
|
7356
7441
|
});
|
|
7357
7442
|
const initialEffort = configOptions.find((option) => option.id === EFFORT_CONFIG_ID);
|
|
@@ -7412,6 +7497,8 @@ export class ClaudeAcpAgent {
|
|
|
7412
7497
|
initialEffort?.currentValue === userProvidedOptions.effort
|
|
7413
7498
|
? userProvidedOptions.effort
|
|
7414
7499
|
: undefined,
|
|
7500
|
+
agents,
|
|
7501
|
+
currentAgent,
|
|
7415
7502
|
fastModeEnabled,
|
|
7416
7503
|
fastModeDisabledReason,
|
|
7417
7504
|
abortController,
|
|
@@ -7844,11 +7931,43 @@ function isValidBaseUrl(baseUrl) {
|
|
|
7844
7931
|
}
|
|
7845
7932
|
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
7846
7933
|
}
|
|
7934
|
+
// `supportedAgents()` always returns Claude Code's built-in subagents — the
|
|
7935
|
+
// ones used for Task-tool delegation (Explore, Plan, etc.) — even when the user
|
|
7936
|
+
// has configured none of their own. Those aren't meaningful *main-thread*
|
|
7937
|
+
// personas, so we filter them out and only surface the Agent picker when the
|
|
7938
|
+
// user (or a plugin/project) has configured custom agents. Update this set if
|
|
7939
|
+
// the SDK's built-in roster changes.
|
|
7940
|
+
export const BUILTIN_AGENT_NAMES = new Set([
|
|
7941
|
+
"claude",
|
|
7942
|
+
"general-purpose",
|
|
7943
|
+
"Explore",
|
|
7944
|
+
"Plan",
|
|
7945
|
+
"statusline-setup",
|
|
7946
|
+
]);
|
|
7947
|
+
// Value of the synthetic "Default" entry in the agent picker, which maps to the
|
|
7948
|
+
// standard Claude Code agent (`applyFlagSettings({ agent: null })`). It is a
|
|
7949
|
+
// reserved sentinel: a custom agent named exactly this would collide with it
|
|
7950
|
+
// (two options sharing the value, selection silently routing to `null`), so we
|
|
7951
|
+
// exclude that name from discovery.
|
|
7952
|
+
/** Discover user/plugin/project-configured main-thread agents, excluding the
|
|
7953
|
+
* built-in subagents and the reserved "default" sentinel. Returns an empty
|
|
7954
|
+
* list if discovery fails so a flaky control request never blocks session
|
|
7955
|
+
* creation. */
|
|
7956
|
+
export async function discoverCustomAgents(q) {
|
|
7957
|
+
try {
|
|
7958
|
+
const agents = await q.supportedAgents();
|
|
7959
|
+
return agents.filter((a) => !BUILTIN_AGENT_NAMES.has(a.name) && a.name !== DEFAULT_AGENT_ID);
|
|
7960
|
+
}
|
|
7961
|
+
catch {
|
|
7962
|
+
return [];
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
7847
7965
|
/** Stable ids for the session config options surfaced via `configOptions`.
|
|
7848
7966
|
* Centralized so the option declarations in `buildConfigOptions` and the
|
|
7849
7967
|
* handlers in `setSessionConfigOption`/`applyConfigOptionValue` reference the
|
|
7850
7968
|
* same identifiers and can't drift apart. */
|
|
7851
7969
|
export { MODE_CONFIG_ID };
|
|
7970
|
+
export const AGENT_CONFIG_ID = "agent";
|
|
7852
7971
|
export const FAST_MODE_CONFIG_ID = "fast";
|
|
7853
7972
|
/** Select-fallback values used when the client has not opted into boolean
|
|
7854
7973
|
* config options (see {@link createFastModeConfigOption}). */
|
|
@@ -7950,7 +8069,7 @@ export function resolveFastModeEnabled(params) {
|
|
|
7950
8069
|
}
|
|
7951
8070
|
throw new Error(`Invalid value for config option ${FAST_MODE_CONFIG_ID}: ${value}`);
|
|
7952
8071
|
}
|
|
7953
|
-
export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel, fastMode, presentation) {
|
|
8072
|
+
export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel, agents = [], currentAgent = DEFAULT_AGENT_ID, fastMode, presentation) {
|
|
7954
8073
|
const options = [
|
|
7955
8074
|
SessionModeManager.configOption(modes),
|
|
7956
8075
|
buildModelConfigOption(models, modelInfos, presentation?.useRecommendedValue === true),
|
|
@@ -7964,6 +8083,27 @@ export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel
|
|
|
7964
8083
|
if (fastMode?.supported) {
|
|
7965
8084
|
options.push(createFastModeConfigOption(fastMode.enabled, fastMode.useBooleanOption, fastMode.disabledReason));
|
|
7966
8085
|
}
|
|
8086
|
+
// Only surface the Agent picker when there's a real choice — i.e. the user
|
|
8087
|
+
// has configured at least one custom agent (built-ins are filtered out in
|
|
8088
|
+
// discoverCustomAgents). With none configured, "Default" would be the only
|
|
8089
|
+
// entry, so we omit the option entirely.
|
|
8090
|
+
if (agents.length > 0) {
|
|
8091
|
+
options.push({
|
|
8092
|
+
id: AGENT_CONFIG_ID,
|
|
8093
|
+
name: "Agent",
|
|
8094
|
+
description: "Main-thread agent persona",
|
|
8095
|
+
type: "select",
|
|
8096
|
+
currentValue: currentAgent,
|
|
8097
|
+
options: [
|
|
8098
|
+
{ value: DEFAULT_AGENT_ID, name: "Default", description: "Standard Claude Code agent" },
|
|
8099
|
+
...agents.map((a) => ({
|
|
8100
|
+
value: a.name,
|
|
8101
|
+
name: a.name,
|
|
8102
|
+
description: a.description || undefined,
|
|
8103
|
+
})),
|
|
8104
|
+
],
|
|
8105
|
+
});
|
|
8106
|
+
}
|
|
7967
8107
|
return options;
|
|
7968
8108
|
}
|
|
7969
8109
|
function getAvailableSlashCommands(commands,
|
|
@@ -8182,6 +8322,31 @@ function isTaskTool(toolName) {
|
|
|
8182
8322
|
function shouldEmitToolCall(toolName) {
|
|
8183
8323
|
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
|
8184
8324
|
}
|
|
8325
|
+
const MAX_DISPATCHED_TOOL_CALLS = 1000;
|
|
8326
|
+
/** Records a tool call that Claude Code sent to the tool runner. */
|
|
8327
|
+
function recordDispatchedToolCall(session, toolCallId) {
|
|
8328
|
+
const calls = (session.dispatchedToolCalls ??= new Set());
|
|
8329
|
+
calls.delete(toolCallId);
|
|
8330
|
+
calls.add(toolCallId);
|
|
8331
|
+
if (calls.size > MAX_DISPATCHED_TOOL_CALLS) {
|
|
8332
|
+
const oldest = calls.values().next().value;
|
|
8333
|
+
if (oldest !== undefined)
|
|
8334
|
+
calls.delete(oldest);
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
/** Records the tool_use blocks of a complete assistant message. */
|
|
8338
|
+
function recordDispatchedToolUses(session, content) {
|
|
8339
|
+
if (!Array.isArray(content))
|
|
8340
|
+
return;
|
|
8341
|
+
for (const block of content) {
|
|
8342
|
+
if ((block?.type === "tool_use" ||
|
|
8343
|
+
block?.type === "server_tool_use" ||
|
|
8344
|
+
block?.type === "mcp_tool_use") &&
|
|
8345
|
+
typeof block.id === "string") {
|
|
8346
|
+
recordDispatchedToolCall(session, block.id);
|
|
8347
|
+
}
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
8185
8350
|
/** Streamed and permission-surfaced tools can precede the SDK's user echo. */
|
|
8186
8351
|
function recordForegroundToolCall(session, toolCallId) {
|
|
8187
8352
|
const turn = session.activeTurn ?? session.turnQueue?.find((queued) => !queued.settled);
|
|
@@ -8201,10 +8366,40 @@ function toolCallFieldsOf(session) {
|
|
|
8201
8366
|
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
8202
8367
|
* Only handles text, image, and thinking chunks for now.
|
|
8203
8368
|
*/
|
|
8369
|
+
/**
|
|
8370
|
+
* The renderer of the notification functions: the agent's tool call
|
|
8371
|
+
* capabilities when they are passed, else those of the ACP capabilities.
|
|
8372
|
+
*/
|
|
8373
|
+
function toolCallRenderer(options) {
|
|
8374
|
+
return options?.toolCallCapabilities
|
|
8375
|
+
? new AcpToolCallRenderer(options.toolCallCapabilities, options.replay)
|
|
8376
|
+
: AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
|
|
8377
|
+
}
|
|
8378
|
+
const MAX_RESOLVED_TOOL_NAMES = 1000;
|
|
8379
|
+
/** Keeps the tool names of the tool results in `content` before the tool use cache drops them. */
|
|
8380
|
+
function rememberResolvedToolNames(session, content) {
|
|
8381
|
+
if (!Array.isArray(content))
|
|
8382
|
+
return;
|
|
8383
|
+
for (const block of content) {
|
|
8384
|
+
if (block?.type !== "tool_result" || typeof block.tool_use_id !== "string")
|
|
8385
|
+
continue;
|
|
8386
|
+
const name = session.toolUseCache[block.tool_use_id]?.name;
|
|
8387
|
+
if (!name)
|
|
8388
|
+
continue;
|
|
8389
|
+
const names = (session.resolvedToolNames ??= new Map());
|
|
8390
|
+
names.delete(block.tool_use_id);
|
|
8391
|
+
names.set(block.tool_use_id, name);
|
|
8392
|
+
if (names.size > MAX_RESOLVED_TOOL_NAMES) {
|
|
8393
|
+
const oldest = names.keys().next().value;
|
|
8394
|
+
if (oldest !== undefined)
|
|
8395
|
+
names.delete(oldest);
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
8398
|
+
}
|
|
8204
8399
|
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
|
|
8205
8400
|
const taskState = options?.taskState ?? new Map();
|
|
8206
8401
|
const registerHooks = options?.registerHooks !== false;
|
|
8207
|
-
const renderer =
|
|
8402
|
+
const renderer = toolCallRenderer(options);
|
|
8208
8403
|
if (typeof content === "string") {
|
|
8209
8404
|
if (content.length === 0) {
|
|
8210
8405
|
return [];
|
|
@@ -8542,6 +8737,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8542
8737
|
const streamedToolInputs = options?.streamedToolInputs;
|
|
8543
8738
|
const forwardedOptions = {
|
|
8544
8739
|
clientCapabilities: options?.clientCapabilities,
|
|
8740
|
+
toolCallCapabilities: options?.toolCallCapabilities,
|
|
8545
8741
|
parentToolUseId: message.parent_tool_use_id,
|
|
8546
8742
|
cwd: options?.cwd,
|
|
8547
8743
|
taskState: options?.taskState,
|
|
@@ -8605,7 +8801,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8605
8801
|
// TodoWrite and the Task* tools never surfaced a tool_call to refine.
|
|
8606
8802
|
if (!shouldEmitToolCall(streamedInput.name))
|
|
8607
8803
|
return [];
|
|
8608
|
-
const update =
|
|
8804
|
+
const update = toolCallRenderer(options).partialRefinement(streamedInput, input, options?.cwd);
|
|
8609
8805
|
if (message.parent_tool_use_id)
|
|
8610
8806
|
stampParentToolUseId(update, message.parent_tool_use_id);
|
|
8611
8807
|
applyMessageId(update, options?.messageId);
|