@agentclientprotocol/claude-agent-acp 0.90.1-preview.1 → 0.90.1-preview.11
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 +5 -2
- package/dist/acp-agent.d.ts +113 -16
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +393 -67
- package/dist/gateway-models.d.ts +62 -0
- package/dist/gateway-models.d.ts.map +1 -0
- package/dist/gateway-models.js +180 -0
- package/dist/native-subagents.d.ts +15 -3
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +21 -4
- package/dist/session-model.d.ts +21 -1
- package/dist/session-model.d.ts.map +1 -1
- package/dist/session-model.js +89 -11
- package/dist/tool-calls/renderer.d.ts +14 -1
- package/dist/tool-calls/renderer.d.ts.map +1 -1
- package/dist/tool-calls/renderer.js +28 -5
- package/dist/turn-events.d.ts +5 -1
- package/dist/turn-events.d.ts.map +1 -1
- package/dist/v2/prompt.d.ts +2 -1
- package/dist/v2/prompt.d.ts.map +1 -1
- package/dist/v2/prompt.js +2 -1
- package/package.json +1 -1
package/dist/acp-agent.js
CHANGED
|
@@ -47,7 +47,8 @@ import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./u
|
|
|
47
47
|
export { DEFAULT_AGENT_ID } from "./session-config-ids.js";
|
|
48
48
|
import { MCP_AVAILABLE_COMMAND, parseMcpCommand, runMcpCommand } from "./mcp-command.js";
|
|
49
49
|
import { MODE_CONFIG_ID, resolveInitialPermissionMode, SessionModeManager, } from "./session-mode.js";
|
|
50
|
-
import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableModels, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
50
|
+
import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableModels, MODEL_CONFIG_ID, narrowToGatewayModels, resolveModelPreference, } from "./session-model.js";
|
|
51
|
+
import { GatewayListingError, listBedrockModels, listGatewayModels, PROVIDER_AUTH_TOKEN_PLACEHOLDER, selectBedrockRegion, } from "./gateway-models.js";
|
|
51
52
|
import { buildEffortConfigOption, EFFORT_CONFIG_ID, effortFlagSettings, mergeEffortSettings, settingsEffortForModel, } from "./session-effort.js";
|
|
52
53
|
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
|
53
54
|
export { applyAvailableModelsAllowlist, matchResumedModel, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
@@ -87,6 +88,11 @@ const STRUCTURED_USAGE_TIMEOUT_MS = 5_000;
|
|
|
87
88
|
* The teardown wakes the consumer itself, so they end within a few ticks
|
|
88
89
|
* unless the consumer is wedged. */
|
|
89
90
|
const TEARDOWN_TURN_END_TIMEOUT_MS = 5_000;
|
|
91
|
+
/** How long `canUseTool` or a hook waits, while a cancel's window may be
|
|
92
|
+
* open, for the consumer to reach a tool use in the stream. Claude Code
|
|
93
|
+
* streams the tool use before it asks, so the consumer is usually there
|
|
94
|
+
* within a few ticks. */
|
|
95
|
+
const TOOL_USE_WAIT_MS = 2_000;
|
|
90
96
|
/** The number of settled subagents whose parent tool call a session keeps for
|
|
91
97
|
* a later resume (see `resumableSubagents`). */
|
|
92
98
|
const MAX_RESUMABLE_SUBAGENTS = 256;
|
|
@@ -285,6 +291,15 @@ const AUTONOMOUS_RESULT_ORIGINS = new Set([
|
|
|
285
291
|
* or a model cycle that Claude Code started on its own. At most one cycle
|
|
286
292
|
* runs at a time, so a constant names it. */
|
|
287
293
|
const AUTONOMOUS_CYCLE = "autonomous-cycle";
|
|
294
|
+
/** The events of a connection that reports no foreground work: v1, whose
|
|
295
|
+
* client has no session state. The agent still tracks the work, which
|
|
296
|
+
* decides what the window after a cancel drops (see `workEndReported`). */
|
|
297
|
+
const UNREPORTED_FOREGROUND = {
|
|
298
|
+
running: () => { },
|
|
299
|
+
awaitingUser: () => { },
|
|
300
|
+
ended: () => { },
|
|
301
|
+
failed: () => { },
|
|
302
|
+
};
|
|
288
303
|
/** Whether this turn's terminal result arrived but its settlement is being
|
|
289
304
|
* held for background subagents it spawned (see Turn.deferredSettle). The
|
|
290
305
|
* single spelling of the hold predicate, shared by the consumer's settle
|
|
@@ -1161,6 +1176,9 @@ export class ClaudeAcpAgent {
|
|
|
1161
1176
|
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
1162
1177
|
* tests can shrink it. */
|
|
1163
1178
|
forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
|
|
1179
|
+
/** How long `canUseTool` or a hook waits for the consumer to reach a tool
|
|
1180
|
+
* use (see `isStoppedWorkToolUse`). Mutable so tests can shrink it. */
|
|
1181
|
+
toolUseWaitMs = TOOL_USE_WAIT_MS;
|
|
1164
1182
|
/**
|
|
1165
1183
|
* The connection serves ACP v2 (`src/v2/`). The agent still speaks v1
|
|
1166
1184
|
* types, except for the tool call content that v2 needs and v1 cannot
|
|
@@ -1171,7 +1189,8 @@ export class ClaudeAcpAgent {
|
|
|
1171
1189
|
foregroundEvents;
|
|
1172
1190
|
/**
|
|
1173
1191
|
* The foreground work of each session while it runs, by the session id that
|
|
1174
|
-
* the client knows
|
|
1192
|
+
* the client knows, on every connection: only the `foreground` option
|
|
1193
|
+
* reports it. A clear-context restart replaces the Claude Code session
|
|
1175
1194
|
* behind that id and moves the running turn to it, so the work belongs to the
|
|
1176
1195
|
* id, not to a {@link Session}.
|
|
1177
1196
|
*/
|
|
@@ -1419,7 +1438,7 @@ export class ClaudeAcpAgent {
|
|
|
1419
1438
|
};
|
|
1420
1439
|
}
|
|
1421
1440
|
async newSession(params) {
|
|
1422
|
-
|
|
1441
|
+
while (this.providerUpdate)
|
|
1423
1442
|
await this.providerUpdate;
|
|
1424
1443
|
const response = await this.createSession(params, {
|
|
1425
1444
|
// Revisit these meta values once we support resume
|
|
@@ -1486,7 +1505,7 @@ export class ClaudeAcpAgent {
|
|
|
1486
1505
|
* later load or resume with the same settings reuses it.
|
|
1487
1506
|
*/
|
|
1488
1507
|
async unstable_forkSession(params) {
|
|
1489
|
-
|
|
1508
|
+
while (this.providerUpdate)
|
|
1490
1509
|
await this.providerUpdate;
|
|
1491
1510
|
const { sessionId } = await forkSession(params, {
|
|
1492
1511
|
liveMessageIdToUuid: this.sessions[params.sessionId]?.messageIdToUuid,
|
|
@@ -1504,7 +1523,7 @@ export class ClaudeAcpAgent {
|
|
|
1504
1523
|
return { sessionId, modes, configOptions };
|
|
1505
1524
|
}
|
|
1506
1525
|
async resumeSession(params) {
|
|
1507
|
-
|
|
1526
|
+
while (this.providerUpdate)
|
|
1508
1527
|
await this.providerUpdate;
|
|
1509
1528
|
const result = await this.getOrCreateSession(params);
|
|
1510
1529
|
this.afterSetupResponse(params.sessionId, params.mcpServers ?? []);
|
|
@@ -1512,7 +1531,7 @@ export class ClaudeAcpAgent {
|
|
|
1512
1531
|
}
|
|
1513
1532
|
async loadSession(params) {
|
|
1514
1533
|
const timing = new SessionTiming(this.logger, "load", params.sessionId);
|
|
1515
|
-
|
|
1534
|
+
while (this.providerUpdate)
|
|
1516
1535
|
await this.providerUpdate;
|
|
1517
1536
|
let result;
|
|
1518
1537
|
if (this.sessions[params.sessionId]) {
|
|
@@ -1894,6 +1913,42 @@ export class ClaudeAcpAgent {
|
|
|
1894
1913
|
// Unknown provider: idempotent success.
|
|
1895
1914
|
return {};
|
|
1896
1915
|
}
|
|
1916
|
+
/**
|
|
1917
|
+
* The model ids the gateway in `config` lists, read before `providers/set`
|
|
1918
|
+
* returns so the sessions it recreates already offer only those, and for
|
|
1919
|
+
* `bedrock` the region prefix picked from them. Setting the same gateway
|
|
1920
|
+
* again reuses the previous listing. `vertex` gateways are not listed.
|
|
1921
|
+
* Throws a {@link GatewayListingError} when the listing fails.
|
|
1922
|
+
*/
|
|
1923
|
+
async listProviderModels(config) {
|
|
1924
|
+
// Reuse needs the very same headers, so a rotated token lists again, and a
|
|
1925
|
+
// failed listing then fails that `providers/set`. Deliberate: AIR keeps its
|
|
1926
|
+
// proxy key for the IDE's lifetime and does not rotate it.
|
|
1927
|
+
const current = this.providerConfig;
|
|
1928
|
+
if (current?.listedModels &&
|
|
1929
|
+
current.apiType === config.apiType &&
|
|
1930
|
+
current.baseUrl === config.baseUrl &&
|
|
1931
|
+
sameHeaders(current.headers, config.headers)) {
|
|
1932
|
+
return {
|
|
1933
|
+
listedModels: current.listedModels,
|
|
1934
|
+
bedrockRegionPrefix: current.bedrockRegionPrefix,
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
if (config.apiType === "anthropic") {
|
|
1938
|
+
return { listedModels: await listGatewayModels(config.baseUrl, config.headers, this.logger) };
|
|
1939
|
+
}
|
|
1940
|
+
if (config.apiType === "bedrock") {
|
|
1941
|
+
const modelIds = await listBedrockModels(config.baseUrl, config.headers, this.logger);
|
|
1942
|
+
if (!modelIds)
|
|
1943
|
+
return {};
|
|
1944
|
+
const region = selectBedrockRegion(modelIds);
|
|
1945
|
+
if (region.modelIds.length < modelIds.length) {
|
|
1946
|
+
this.logger.log(`[providers/set] Bedrock models span several region prefixes; using ${region.regionPrefix} and dropping ${modelIds.length - region.modelIds.length} id(s) of other prefixes`);
|
|
1947
|
+
}
|
|
1948
|
+
return { listedModels: region.modelIds, bedrockRegionPrefix: region.regionPrefix };
|
|
1949
|
+
}
|
|
1950
|
+
return {};
|
|
1951
|
+
}
|
|
1897
1952
|
resolveProviderConfig() {
|
|
1898
1953
|
return this.providerConfig ?? gatewayRequestToProviderConfig(this.gatewayAuthRequest);
|
|
1899
1954
|
}
|
|
@@ -1927,7 +1982,9 @@ export class ClaudeAcpAgent {
|
|
|
1927
1982
|
// provider routing set via `providers/set`. Neither touches the on-disk
|
|
1928
1983
|
// credential store, so dropping these references is the whole logout for
|
|
1929
1984
|
// those paths.
|
|
1930
|
-
|
|
1985
|
+
// A queued `providers/set` may still be listing and apply after this point,
|
|
1986
|
+
// so a busy queue counts as an override to clear.
|
|
1987
|
+
const hadRoutingOverride = this.resolveProviderConfig() !== null || this.providerUpdate !== null;
|
|
1931
1988
|
this.gatewayAuthRequest = undefined;
|
|
1932
1989
|
this.providerConfig = undefined;
|
|
1933
1990
|
// Any probe already running read the pre-logout world; the bump makes its
|
|
@@ -2032,7 +2089,7 @@ export class ClaudeAcpAgent {
|
|
|
2032
2089
|
* before this resolves.
|
|
2033
2090
|
*/
|
|
2034
2091
|
async startTurn(params, events) {
|
|
2035
|
-
|
|
2092
|
+
while (this.providerUpdate)
|
|
2036
2093
|
await this.providerUpdate;
|
|
2037
2094
|
let session = this.sessions[params.sessionId];
|
|
2038
2095
|
if (!session) {
|
|
@@ -3150,8 +3207,14 @@ export class ClaudeAcpAgent {
|
|
|
3150
3207
|
/** Sends `report`, which ends the tool call `toolCallId` without its result
|
|
3151
3208
|
* (see `AcpToolCallRenderer.unfinished`), and forgets the call. Forgetting
|
|
3152
3209
|
* comes first, so that a late hook or a second close cannot overwrite the
|
|
3153
|
-
* report.
|
|
3210
|
+
* report. A command has a display terminal on a client that renders them,
|
|
3211
|
+
* and no result will end it, so the report ends it too. */
|
|
3154
3212
|
const closeToolCall = async (toolCallId, report) => {
|
|
3213
|
+
// Read before the cache entry goes: it names the tool.
|
|
3214
|
+
const toolUse = session.toolUseCache[toolCallId];
|
|
3215
|
+
const terminalExit = toolUse && new AcpToolCallRenderer(this.toolCallCapabilities).terminalExit(toolUse);
|
|
3216
|
+
if (terminalExit)
|
|
3217
|
+
report._meta = { ...report._meta, ...terminalExit };
|
|
3155
3218
|
unregisterHookCallback(toolCallId);
|
|
3156
3219
|
session.emittedToolCalls.delete(toolCallId);
|
|
3157
3220
|
delete session.toolUseCache[toolCallId];
|
|
@@ -3166,26 +3229,24 @@ export class ClaudeAcpAgent {
|
|
|
3166
3229
|
task.backgrounded === true &&
|
|
3167
3230
|
!task.endedPerLevel &&
|
|
3168
3231
|
task.parentToolUseId === agentToolUseId);
|
|
3169
|
-
/**
|
|
3170
|
-
*
|
|
3171
|
-
*
|
|
3172
|
-
*
|
|
3173
|
-
*
|
|
3174
|
-
*
|
|
3175
|
-
*
|
|
3176
|
-
*
|
|
3177
|
-
*
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
*
|
|
3181
|
-
*
|
|
3182
|
-
|
|
3183
|
-
* for the first (see `Turn.closingCancelledToolCalls`). */
|
|
3184
|
-
const closeCancelledToolCalls = (turn) => (turn.closingCancelledToolCalls ??= (async () => {
|
|
3232
|
+
/** Whether output of the conversation `parentToolUseId` (null for the main
|
|
3233
|
+
* one) is a background subagent's that still runs. A cancel does not stop
|
|
3234
|
+
* such a subagent's work (agentclientprotocol/agent-client-protocol#2308):
|
|
3235
|
+
* the closing at the cancel leaves its tool calls open for their results
|
|
3236
|
+
* (`closedAtCancel`), and the window after the cancel lets its output
|
|
3237
|
+
* through (see the `session.cancelled` checks of the stream and the
|
|
3238
|
+
* user/assistant cases). Claude Code currently stops background subagents
|
|
3239
|
+
* at an interrupt, so this decides only the output that comes before the
|
|
3240
|
+
* SDK reports a subagent's end. */
|
|
3241
|
+
const isRunningBackgroundSubagentOutput = (parentToolUseId) => parentToolUseId !== null && runsInBackground(parentToolUseId);
|
|
3242
|
+
/** Ends the open tool calls that `include` accepts, with the report of a call
|
|
3243
|
+
* that a cancel stopped and sent no result for. `include` gets the Agent/Task
|
|
3244
|
+
* call of the subagent that made the tool call, if a subagent did. */
|
|
3245
|
+
const closeOpenToolCalls = async (include) => {
|
|
3185
3246
|
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
|
3186
3247
|
for (const toolCallId of [...session.emittedToolCalls]) {
|
|
3187
3248
|
const parentToolUseId = session.toolCallParents?.get(toolCallId);
|
|
3188
|
-
if (
|
|
3249
|
+
if (!include(parentToolUseId))
|
|
3189
3250
|
continue;
|
|
3190
3251
|
const report = renderer.unfinished({
|
|
3191
3252
|
toolCallId,
|
|
@@ -3198,8 +3259,48 @@ export class ClaudeAcpAgent {
|
|
|
3198
3259
|
stampParentToolUseId(report, parentToolUseId);
|
|
3199
3260
|
await closeToolCall(toolCallId, report);
|
|
3200
3261
|
}
|
|
3201
|
-
}
|
|
3262
|
+
};
|
|
3263
|
+
/** Whether the closing at the end of the work a cancel stopped takes a tool
|
|
3264
|
+
* call that the subagent started by the Agent call `parentToolUseId` made
|
|
3265
|
+
* (undefined for a call of the main conversation): not one of a running
|
|
3266
|
+
* background subagent, which the cancel may not have stopped. */
|
|
3267
|
+
const closedAtCancel = (parentToolUseId) => parentToolUseId === undefined || !runsInBackground(parentToolUseId);
|
|
3268
|
+
/** Closes the tool calls that the work a cancel stopped left open, as that
|
|
3269
|
+
* work ends. Claude Code's interrupt ends each running tool of the main
|
|
3270
|
+
* conversation with an error result, which `sendInterruptedToolResults`
|
|
3271
|
+
* delivers. What is still open here has no result to deliver:
|
|
3272
|
+
* - a tool of a subagent: Claude Code writes its interrupted result only to
|
|
3273
|
+
* the subagent's own transcript, not to the stream;
|
|
3274
|
+
* - a tool call that Claude abandoned before it ran;
|
|
3275
|
+
* - every tool call, when the stream gives no results at all, as when the
|
|
3276
|
+
* force-cancel backstop ends a wedged turn.
|
|
3277
|
+
* A foreground subagent ends with the work that waits on it. A background
|
|
3278
|
+
* subagent keeps its tool calls open until the SDK reports its end, which
|
|
3279
|
+
* Claude Code 2.1.293 does for the ones it stops, before the turn's result
|
|
3280
|
+
* (see `NativeSubagentRuntime.parentCancelled`). When the SDK reports it
|
|
3281
|
+
* only after the work ended, `closeStoppedSubagentToolCalls` closes them.
|
|
3282
|
+
* Runs once per turn: a second call, from the other settling path, waits
|
|
3283
|
+
* for the first (see `Turn.closingCancelledToolCalls`). */
|
|
3284
|
+
const closeCancelledToolCalls = (turn) => (turn.closingCancelledToolCalls ??= closeOpenToolCalls(closedAtCancel));
|
|
3202
3285
|
session.closeCancelledToolCalls = closeCancelledToolCalls;
|
|
3286
|
+
/** Closes the tool calls of the subagent that the Agent call `agentToolUseId`
|
|
3287
|
+
* started, once the SDK reports the subagent stopped, if a cancel stopped it
|
|
3288
|
+
* after the work that waited on it ended. Nothing else ends them then.
|
|
3289
|
+
* Example: the cancel settles a turn that v1 holds for a background
|
|
3290
|
+
* subagent, so `closeCancelledToolCalls` leaves the subagent's command
|
|
3291
|
+
* open, since the subagent runs. Claude Code's interrupt then stops the
|
|
3292
|
+
* subagent. Claude Code 2.1.296 sends the command's interrupted result
|
|
3293
|
+
* after that, but the result is dropped like all output after the work's
|
|
3294
|
+
* end (see the `session.cancelled` check of the user/assistant case).
|
|
3295
|
+
* While the work still runs, the calls of a stopped subagent get Claude
|
|
3296
|
+
* Code's result, or `closeCancelledToolCalls` at the work's end. */
|
|
3297
|
+
const closeStoppedSubagentToolCalls = async (agentToolUseId) => {
|
|
3298
|
+
if (!agentToolUseId || !session.cancelled)
|
|
3299
|
+
return;
|
|
3300
|
+
if (!this.workEndReported(params.sessionId, session))
|
|
3301
|
+
return;
|
|
3302
|
+
await closeOpenToolCalls((parentToolUseId) => parentToolUseId === agentToolUseId);
|
|
3303
|
+
};
|
|
3203
3304
|
/** Delivers the results that the work a cancel stopped sends for tool calls
|
|
3204
3305
|
* that the client holds open. Claude Code's interrupt ends each running tool
|
|
3205
3306
|
* of the main conversation with an error result, before the result that
|
|
@@ -3233,6 +3334,47 @@ export class ClaudeAcpAgent {
|
|
|
3233
3334
|
await sendUpdate(notification);
|
|
3234
3335
|
}
|
|
3235
3336
|
};
|
|
3337
|
+
/** The model cycle that Claude Code started on its own ends, and its end is
|
|
3338
|
+
* about to be reported: closes the tool calls that it left open first. A
|
|
3339
|
+
* cycle is foreground work that no turn settles, so neither
|
|
3340
|
+
* `closeCancelledToolCalls` nor the check of `settleActive` for unfinished
|
|
3341
|
+
* tools runs for it. A turn that took the cycle over closes its own calls,
|
|
3342
|
+
* when it settles.
|
|
3343
|
+
*
|
|
3344
|
+
* After a cancel, Claude Code's interrupt gives the tools of the main
|
|
3345
|
+
* conversation results, which `sendInterruptedToolResults` delivers while
|
|
3346
|
+
* the cycle runs. What is still open has none, such as the command of a
|
|
3347
|
+
* foreground subagent. The follow-up of a turn that v1 holds for its
|
|
3348
|
+
* background subagents is a cycle too, and the cancel ends the held turn
|
|
3349
|
+
* as well, possibly in `cancelTurns` while these reports are in flight. So
|
|
3350
|
+
* the cycle's calls close with the held turn's closing, which each settle
|
|
3351
|
+
* of the turn waits for, and the response never comes before them.
|
|
3352
|
+
*
|
|
3353
|
+
* At any other end, every call that ran has its result. What is open is a
|
|
3354
|
+
* call of the main conversation that Claude abandoned before it ran, such
|
|
3355
|
+
* as a streamed tool_use that never reached a complete message, which a
|
|
3356
|
+
* turn's end closes the same way (`closeUnfinished`). The calls of a
|
|
3357
|
+
* subagent are not the cycle's: a background subagent runs on. */
|
|
3358
|
+
const closeCycleToolCalls = async () => {
|
|
3359
|
+
if (!this.runsAutonomousCycle(params.sessionId))
|
|
3360
|
+
return;
|
|
3361
|
+
if (session.cancelled) {
|
|
3362
|
+
const held = session.activeTurn;
|
|
3363
|
+
if (held && !held.settled)
|
|
3364
|
+
await closeCancelledToolCalls(held);
|
|
3365
|
+
else
|
|
3366
|
+
await closeOpenToolCalls(closedAtCancel);
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
|
|
3370
|
+
for (const toolCallId of [...session.emittedToolCalls]) {
|
|
3371
|
+
if (session.toolCallParents?.has(toolCallId))
|
|
3372
|
+
continue;
|
|
3373
|
+
if (session.dispatchedToolCalls?.has(toolCallId))
|
|
3374
|
+
continue;
|
|
3375
|
+
await closeToolCall(toolCallId, renderer.unfinished({ toolCallId, ran: false }));
|
|
3376
|
+
}
|
|
3377
|
+
};
|
|
3236
3378
|
/** At the actual turn boundary, preview its checkpoint, settle the active
|
|
3237
3379
|
* turn exactly once, disarm the force-cancel backstop, and drop it from
|
|
3238
3380
|
* the queue. Cancellation and provider failures skip checkpoint I/O. */
|
|
@@ -3882,6 +4024,7 @@ export class ClaudeAcpAgent {
|
|
|
3882
4024
|
// here. An owed idle belongs to an earlier result: it can lag
|
|
3883
4025
|
// past the cycle's first output.
|
|
3884
4026
|
if (session.owedTrailingIdles === 0) {
|
|
4027
|
+
await closeCycleToolCalls();
|
|
3885
4028
|
this.endAutonomousCycle(params.sessionId, {
|
|
3886
4029
|
outcome: { stopReason: session.cancelled ? "cancelled" : "end_turn" },
|
|
3887
4030
|
});
|
|
@@ -4232,6 +4375,9 @@ export class ClaudeAcpAgent {
|
|
|
4232
4375
|
droppedStreams.delete(message.tool_use_id);
|
|
4233
4376
|
}
|
|
4234
4377
|
await endBackgroundTerminal(message.task_id);
|
|
4378
|
+
if (message.status === "stopped") {
|
|
4379
|
+
await closeStoppedSubagentToolCalls(message.tool_use_id);
|
|
4380
|
+
}
|
|
4235
4381
|
settleLiveTask(message.task_id);
|
|
4236
4382
|
break;
|
|
4237
4383
|
case "task_updated":
|
|
@@ -4263,6 +4409,9 @@ export class ClaudeAcpAgent {
|
|
|
4263
4409
|
droppedStreams.delete(parentToolUseId);
|
|
4264
4410
|
}
|
|
4265
4411
|
await endBackgroundTerminal(message.task_id);
|
|
4412
|
+
if (message.patch.status === "killed") {
|
|
4413
|
+
await closeStoppedSubagentToolCalls(parentToolUseId);
|
|
4414
|
+
}
|
|
4266
4415
|
settleLiveTask(message.task_id);
|
|
4267
4416
|
}
|
|
4268
4417
|
else if (message.patch.status === "running" ||
|
|
@@ -4698,6 +4847,7 @@ export class ClaudeAcpAgent {
|
|
|
4698
4847
|
pendingExitPlanContextReset?.toolUseId ===
|
|
4699
4848
|
pendingExitPlanModeInterruption?.toolUseId;
|
|
4700
4849
|
if (!continuesPlan && !(isAutonomousResult && message.num_turns === 0)) {
|
|
4850
|
+
await closeCycleToolCalls();
|
|
4701
4851
|
this.endAutonomousCycle(params.sessionId, autonomousCycleEnd(message, resultModelUsage, session.cancelled || exitPlanInterruption || isAbortedResult(message)));
|
|
4702
4852
|
}
|
|
4703
4853
|
if (session.cancelled) {
|
|
@@ -5156,7 +5306,8 @@ export class ClaudeAcpAgent {
|
|
|
5156
5306
|
// `cancelled` check in the user/assistant case). Otherwise the
|
|
5157
5307
|
// answer to a prompt the cancel abandoned, which Claude Code had
|
|
5158
5308
|
// already taken off its queue, would stream to the client after the
|
|
5159
|
-
// cancel was reported.
|
|
5309
|
+
// cancel was reported. A background subagent that still runs is not
|
|
5310
|
+
// work the cancel stopped (see isRunningBackgroundSubagentOutput).
|
|
5160
5311
|
//
|
|
5161
5312
|
// Where the consolidated message delivers the text (the same
|
|
5162
5313
|
// condition as in the user/assistant case), a stream that loses a
|
|
@@ -5168,7 +5319,13 @@ export class ClaudeAcpAgent {
|
|
|
5168
5319
|
// prefix of the text, so the text would be sent again (see
|
|
5169
5320
|
// unstreamedRemainder).
|
|
5170
5321
|
const streamKey = message.parent_tool_use_id ?? "";
|
|
5171
|
-
|
|
5322
|
+
const stoppedWorkOutput = this.dropsStoppedWorkOutput(params.sessionId, session) &&
|
|
5323
|
+
!isRunningBackgroundSubagentOutput(message.parent_tool_use_id);
|
|
5324
|
+
if (message.event.type === "content_block_start" &&
|
|
5325
|
+
isToolUseBlock(message.event.content_block)) {
|
|
5326
|
+
recordReachedToolUse(session, message.event.content_block.id, stoppedWorkOutput);
|
|
5327
|
+
}
|
|
5328
|
+
if (stoppedWorkOutput) {
|
|
5172
5329
|
if (message.parent_tool_use_id === null || forwardsSubagentText()) {
|
|
5173
5330
|
droppedStreams.add(streamKey);
|
|
5174
5331
|
}
|
|
@@ -5238,8 +5395,18 @@ export class ClaudeAcpAgent {
|
|
|
5238
5395
|
}
|
|
5239
5396
|
case "user":
|
|
5240
5397
|
case "assistant": {
|
|
5241
|
-
if (message.type === "assistant")
|
|
5398
|
+
if (message.type === "assistant") {
|
|
5242
5399
|
recordDispatchedToolUses(session, message.message.content);
|
|
5400
|
+
// A tool use that did not stream is reached here first.
|
|
5401
|
+
const content = message.message.content;
|
|
5402
|
+
const stoppedWorkOutput = this.dropsStoppedWorkOutput(params.sessionId, session) &&
|
|
5403
|
+
!isRunningBackgroundSubagentOutput(message.parent_tool_use_id);
|
|
5404
|
+
for (const block of Array.isArray(content) ? content : []) {
|
|
5405
|
+
if (isToolUseBlock(block)) {
|
|
5406
|
+
recordReachedToolUse(session, block.id, stoppedWorkOutput);
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5243
5410
|
// Record the ACP messageId -> SDK uuid mapping for this message
|
|
5244
5411
|
// (including replays). The consolidated message carries both ids, so
|
|
5245
5412
|
// this is where we learn the uuid the SDK's rewind/resume APIs key on
|
|
@@ -5343,11 +5510,13 @@ export class ClaudeAcpAgent {
|
|
|
5343
5510
|
break;
|
|
5344
5511
|
}
|
|
5345
5512
|
}
|
|
5346
|
-
if (session.cancelled
|
|
5513
|
+
if (session.cancelled &&
|
|
5514
|
+
!isRunningBackgroundSubagentOutput(message.parent_tool_use_id)) {
|
|
5347
5515
|
// The work the cancel stopped ends its tools with error results.
|
|
5348
5516
|
// Until the client is told that the work ended, they close the
|
|
5349
5517
|
// tool calls it opened. Everything else that work sends is
|
|
5350
|
-
// dropped, and so is everything after that report.
|
|
5518
|
+
// dropped, and so is everything after that report. A background
|
|
5519
|
+
// subagent that still runs is not that work: its messages go on.
|
|
5351
5520
|
if (message.type === "user" && !this.workEndReported(params.sessionId, session)) {
|
|
5352
5521
|
await sendInterruptedToolResults(message);
|
|
5353
5522
|
}
|
|
@@ -5778,6 +5947,10 @@ export class ClaudeAcpAgent {
|
|
|
5778
5947
|
return;
|
|
5779
5948
|
}
|
|
5780
5949
|
session.cancelled = true;
|
|
5950
|
+
// The interrupt below makes Claude Code withdraw the requests that are open
|
|
5951
|
+
// now (see `withPendingUserInput`).
|
|
5952
|
+
for (const input of session.openUserInputs ?? [])
|
|
5953
|
+
input.cancelled = true;
|
|
5781
5954
|
// Before anything awaits, so a model request that the consumer reads in
|
|
5782
5955
|
// the meantime is already judged against these prompts.
|
|
5783
5956
|
session.cancelledPrompts ??= new Set();
|
|
@@ -6706,19 +6879,29 @@ export class ClaudeAcpAgent {
|
|
|
6706
6879
|
/** Runs `request`, a request that `requestSessionId` sends for the session
|
|
6707
6880
|
* `ownerSessionId`. When that is a subagent's child session, the child
|
|
6708
6881
|
* reports that it awaits the user while the request is open (the RFD form,
|
|
6709
|
-
* see `NativeSubagentRuntime.awaitingUser`).
|
|
6710
|
-
|
|
6882
|
+
* see `NativeSubagentRuntime.awaitingUser`). `signal` aborts when Claude
|
|
6883
|
+
* Code withdraws the request. */
|
|
6884
|
+
withChildAwaitingUser(ownerSessionId, requestSessionId, signal, request) {
|
|
6711
6885
|
const runtime = this.sessions[ownerSessionId]?.nativeSubagentRuntime;
|
|
6712
6886
|
return runtime && requestSessionId !== ownerSessionId
|
|
6713
|
-
? runtime.awaitingUser(requestSessionId, request)
|
|
6887
|
+
? runtime.awaitingUser(requestSessionId, request, signal)
|
|
6714
6888
|
: request();
|
|
6715
6889
|
}
|
|
6716
6890
|
/** Mark a client request of the subagent `agentID` (absent for the main
|
|
6717
6891
|
* conversation) as awaiting user input for exactly the lifetime of its
|
|
6718
6892
|
* promise (see `Session.openUserInputs`). Steering consults the open
|
|
6719
6893
|
* requests synchronously, so a message arriving while any permission or
|
|
6720
|
-
* elicitation card is open uses non-interrupting SDK delivery.
|
|
6721
|
-
|
|
6894
|
+
* elicitation card is open uses non-interrupting SDK delivery.
|
|
6895
|
+
*
|
|
6896
|
+
* `signal` aborts when Claude Code withdraws the request, which is not an
|
|
6897
|
+
* answer. Example: the user cancels while a permission request is open.
|
|
6898
|
+
* Claude Code's interrupt withdraws it as it stops the work, and the work
|
|
6899
|
+
* ends `idle cancelled` right after. The end of the request then reports
|
|
6900
|
+
* no `running`: the client would see `requires_action`, `running`, `idle
|
|
6901
|
+
* cancelled`. A withdrawal without a cancel means that something else
|
|
6902
|
+
* answered the request in the user's place, such as a `PermissionRequest`
|
|
6903
|
+
* hook, and the work goes on, so the end reports `running`. */
|
|
6904
|
+
async withPendingUserInput(sessionId, signal, request, agentID) {
|
|
6722
6905
|
const session = this.sessions[sessionId];
|
|
6723
6906
|
if (!session)
|
|
6724
6907
|
return request();
|
|
@@ -6730,7 +6913,8 @@ export class ClaudeAcpAgent {
|
|
|
6730
6913
|
}
|
|
6731
6914
|
finally {
|
|
6732
6915
|
session.openUserInputs.delete(input);
|
|
6733
|
-
|
|
6916
|
+
const withdrawnByCancel = signal.aborted && input.cancelled === true;
|
|
6917
|
+
this.syncAwaitingUser(sessionId, session, withdrawnByCancel);
|
|
6734
6918
|
}
|
|
6735
6919
|
}
|
|
6736
6920
|
/** Report whether the session's foreground work awaits the user: exactly
|
|
@@ -6739,8 +6923,13 @@ export class ClaudeAcpAgent {
|
|
|
6739
6923
|
* and Claude Code can ask for a queued prompt before the consumer activates
|
|
6740
6924
|
* it, so a request is not tied to the work that was running when it
|
|
6741
6925
|
* opened. Called when a request opens or closes, when a turn activates, and
|
|
6742
|
-
* when a task registers or moves to the background.
|
|
6743
|
-
|
|
6926
|
+
* when a task registers or moves to the background.
|
|
6927
|
+
*
|
|
6928
|
+
* When the last request ends, the work reports `running` again, unless a
|
|
6929
|
+
* cancel withdrew that request (`withdrawnByCancel`, see
|
|
6930
|
+
* {@link withPendingUserInput}): the work is being stopped, and reports its
|
|
6931
|
+
* end next. */
|
|
6932
|
+
syncAwaitingUser(sessionId, session, withdrawnByCancel = false) {
|
|
6744
6933
|
const work = this.foregroundWork.get(sessionId);
|
|
6745
6934
|
if (!work)
|
|
6746
6935
|
return;
|
|
@@ -6750,7 +6939,7 @@ export class ClaudeAcpAgent {
|
|
|
6750
6939
|
work.awaitingUser = waiting;
|
|
6751
6940
|
if (waiting)
|
|
6752
6941
|
work.events.awaitingUser();
|
|
6753
|
-
else
|
|
6942
|
+
else if (!withdrawnByCancel)
|
|
6754
6943
|
work.events.running();
|
|
6755
6944
|
}
|
|
6756
6945
|
/** Make `owner` the foreground work of the session `sessionId` (see
|
|
@@ -6759,14 +6948,12 @@ export class ClaudeAcpAgent {
|
|
|
6759
6948
|
* reports nothing new: a turn takes over a cycle that Claude Code started on
|
|
6760
6949
|
* its own, and only the owner's end ends the work. */
|
|
6761
6950
|
startForeground(sessionId, owner) {
|
|
6762
|
-
if (!this.foregroundEvents)
|
|
6763
|
-
return;
|
|
6764
6951
|
const current = this.foregroundWork.get(sessionId);
|
|
6765
6952
|
if (current) {
|
|
6766
6953
|
current.owner = owner;
|
|
6767
6954
|
return;
|
|
6768
6955
|
}
|
|
6769
|
-
const events = this.foregroundEvents(sessionId);
|
|
6956
|
+
const events = this.foregroundEvents?.(sessionId) ?? UNREPORTED_FOREGROUND;
|
|
6770
6957
|
this.foregroundWork.set(sessionId, { owner, events, awaitingUser: false });
|
|
6771
6958
|
events.running();
|
|
6772
6959
|
const session = this.sessions[sessionId];
|
|
@@ -6792,7 +6979,7 @@ export class ClaudeAcpAgent {
|
|
|
6792
6979
|
* whose work ended at its result, so its follow-up is a cycle. Output of
|
|
6793
6980
|
* the work a cancel stopped starts nothing (see Session.cancelled). */
|
|
6794
6981
|
startAutonomousCycle(sessionId, session, message) {
|
|
6795
|
-
if (
|
|
6982
|
+
if (this.foregroundWork.has(sessionId) || session.cancelled)
|
|
6796
6983
|
return;
|
|
6797
6984
|
if (message.type !== "assistant" && message.type !== "stream_event")
|
|
6798
6985
|
return;
|
|
@@ -6810,15 +6997,79 @@ export class ClaudeAcpAgent {
|
|
|
6810
6997
|
/** Whether the client has been told that the session's work ended: on v2 by
|
|
6811
6998
|
* an idle state, so no foreground work is open, including a cycle that
|
|
6812
6999
|
* Claude Code started on its own; on v1 by the response to the turn's
|
|
6813
|
-
* prompt, so no turn is active.
|
|
7000
|
+
* prompt, so no turn is active. v1 reports nothing for a cycle, so its end
|
|
7001
|
+
* is its result or idle: until then, Claude Code's results for the cycle's
|
|
7002
|
+
* tool calls still reach the client after a cancel. */
|
|
6814
7003
|
workEndReported(sessionId, session) {
|
|
6815
|
-
return this.foregroundEvents
|
|
7004
|
+
return this.foregroundEvents
|
|
7005
|
+
? !this.foregroundWork.has(sessionId)
|
|
7006
|
+
: !session.activeTurn && !this.runsAutonomousCycle(sessionId);
|
|
7007
|
+
}
|
|
7008
|
+
/** Whether the consumer drops what it reaches now as output of the work a
|
|
7009
|
+
* cancel stopped: the client has been told that the work ended, and the
|
|
7010
|
+
* window after the cancel is still open (see Session.cancelled). */
|
|
7011
|
+
dropsStoppedWorkOutput(sessionId, session) {
|
|
7012
|
+
return session.cancelled && this.workEndReported(sessionId, session);
|
|
7013
|
+
}
|
|
7014
|
+
/**
|
|
7015
|
+
* Whether the tool use `toolUseId`, which reaches `canUseTool` or a hook, is
|
|
7016
|
+
* output of the work a cancel stopped that the consumer dropped (see
|
|
7017
|
+
* `dropsStoppedWorkOutput`). Example: Claude Code runs a prompt that the
|
|
7018
|
+
* cancel abandoned, and it asks to run a command. The consumer drops the
|
|
7019
|
+
* tool use and its result, so a tool call shown for it would never end, and
|
|
7020
|
+
* the client would keep it open (issue #1061).
|
|
7021
|
+
*
|
|
7022
|
+
* Claude Code calls `canUseTool` and hooks on its control channel, after it
|
|
7023
|
+
* streamed the tool use, and the consumer can lag behind. So the session can
|
|
7024
|
+
* still look cancelled while the consumer has the next prompt's echo, which
|
|
7025
|
+
* ends the window, still ahead of the tool use. While a cancel's window may
|
|
7026
|
+
* be open, this waits until the consumer reaches the tool use, for at most
|
|
7027
|
+
* {@link toolUseWaitMs} or until `signal` aborts: the stream never shows
|
|
7028
|
+
* some tool uses, such as SandboxNetworkAccess's. Without a cancel nothing
|
|
7029
|
+
* is dropped, so it answers at once. It returns a promise only when it
|
|
7030
|
+
* waits, so a permission request without a cancel goes out as soon as
|
|
7031
|
+
* before.
|
|
7032
|
+
*/
|
|
7033
|
+
isStoppedWorkToolUse(session, toolUseId, signal) {
|
|
7034
|
+
const reached = session.reachedToolUses?.get(toolUseId);
|
|
7035
|
+
if (reached !== undefined)
|
|
7036
|
+
return reached;
|
|
7037
|
+
if (!session.cancelled || signal?.aborted)
|
|
7038
|
+
return false;
|
|
7039
|
+
return this.reachedInStream(session, toolUseId, signal);
|
|
7040
|
+
}
|
|
7041
|
+
/** Waits until the consumer reaches the tool use `toolUseId` (see
|
|
7042
|
+
* `isStoppedWorkToolUse`), then says whether it dropped it. */
|
|
7043
|
+
async reachedInStream(session, toolUseId, signal) {
|
|
7044
|
+
await new Promise((resolve) => {
|
|
7045
|
+
const waiters = (session.toolUseWaiters ??= new Map());
|
|
7046
|
+
let waiting = waiters.get(toolUseId);
|
|
7047
|
+
if (!waiting)
|
|
7048
|
+
waiters.set(toolUseId, (waiting = new Set()));
|
|
7049
|
+
const done = () => {
|
|
7050
|
+
clearTimeout(timer);
|
|
7051
|
+
signal?.removeEventListener("abort", done);
|
|
7052
|
+
waiting.delete(done);
|
|
7053
|
+
if (waiting.size === 0 && waiters.get(toolUseId) === waiting)
|
|
7054
|
+
waiters.delete(toolUseId);
|
|
7055
|
+
resolve();
|
|
7056
|
+
};
|
|
7057
|
+
const timer = setTimeout(done, this.toolUseWaitMs);
|
|
7058
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
7059
|
+
waiting.add(done);
|
|
7060
|
+
});
|
|
7061
|
+
return session.reachedToolUses?.get(toolUseId) ?? false;
|
|
6816
7062
|
}
|
|
6817
7063
|
/** End the session's model cycle that Claude Code started on its own, if one
|
|
6818
7064
|
* runs. */
|
|
6819
7065
|
endAutonomousCycle(sessionId, end) {
|
|
6820
7066
|
this.endForeground(sessionId, AUTONOMOUS_CYCLE, end);
|
|
6821
7067
|
}
|
|
7068
|
+
/** Whether a model cycle that Claude Code started on its own is the session's
|
|
7069
|
+
* foreground work. A turn that took a cycle over owns the work from then on. */
|
|
7070
|
+
runsAutonomousCycle(sessionId) {
|
|
7071
|
+
return this.foregroundWork.get(sessionId)?.owner === AUTONOMOUS_CYCLE;
|
|
7072
|
+
}
|
|
6822
7073
|
/** Forward a permission request to the client, wiring the tool call's
|
|
6823
7074
|
* `signal` through as a `cancellationSignal`. When the turn is cancelled
|
|
6824
7075
|
* while the client's prompt is still open the signal aborts, the SDK sends
|
|
@@ -6843,7 +7094,7 @@ export class ClaudeAcpAgent {
|
|
|
6843
7094
|
// The request goes to a subagent's child session for a tool call of that
|
|
6844
7095
|
// subagent, but it blocks the turn of the owner session.
|
|
6845
7096
|
try {
|
|
6846
|
-
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, () => this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID));
|
|
7097
|
+
return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, signal, () => this.withPendingUserInput(ownerSessionId, signal, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID));
|
|
6847
7098
|
}
|
|
6848
7099
|
catch (error) {
|
|
6849
7100
|
if (signal.aborted) {
|
|
@@ -6852,6 +7103,17 @@ export class ClaudeAcpAgent {
|
|
|
6852
7103
|
throw error;
|
|
6853
7104
|
}
|
|
6854
7105
|
}
|
|
7106
|
+
/** Send the client an elicitation for a callback of Claude Code. When the
|
|
7107
|
+
* callback's `signal` aborts, the SDK sends `$/cancel_request` and keeps
|
|
7108
|
+
* waiting for the client's reply, which a client that ignores the cancel
|
|
7109
|
+
* never sends. So the call also rejects at once on the abort, as
|
|
7110
|
+
* {@link requestPermissionFromClient} does for a permission request. Left
|
|
7111
|
+
* open, the request would stay in `openUserInputs` (see
|
|
7112
|
+
* {@link withPendingUserInput}), which keeps steering non-interrupting, and
|
|
7113
|
+
* a child's `openRequests` would not drop. */
|
|
7114
|
+
elicitFromClient(request, signal) {
|
|
7115
|
+
return raceWithAbort(this.client.createElicitation(request, signal), signal);
|
|
7116
|
+
}
|
|
6855
7117
|
/** Emit the `tool_call` a permission request references if it hasn't been sent
|
|
6856
7118
|
* yet, so the client has the tool call before being asked to approve it. The
|
|
6857
7119
|
* matching streamed tool_use chunk later refines it with a `tool_call_update`
|
|
@@ -6948,17 +7210,21 @@ export class ClaudeAcpAgent {
|
|
|
6948
7210
|
* decision, and a failure only leaves the preview out.
|
|
6949
7211
|
*/
|
|
6950
7212
|
fileChangePreviewHook(sessionId) {
|
|
6951
|
-
return async (input, toolUseID) => {
|
|
7213
|
+
return async (input, toolUseID, { signal }) => {
|
|
6952
7214
|
if (input.hook_event_name === "PreToolUse" && toolUseID) {
|
|
6953
|
-
await this.previewFileChange(sessionId, input, toolUseID).catch((error) => this.logger.error(`Session ${sessionId}: failed to preview ${toolUseID}: ${error}`));
|
|
7215
|
+
await this.previewFileChange(sessionId, input, toolUseID, signal).catch((error) => this.logger.error(`Session ${sessionId}: failed to preview ${toolUseID}: ${error}`));
|
|
6954
7216
|
}
|
|
6955
7217
|
return { continue: true };
|
|
6956
7218
|
};
|
|
6957
7219
|
}
|
|
6958
|
-
async previewFileChange(sessionId, input, toolUseID) {
|
|
7220
|
+
async previewFileChange(sessionId, input, toolUseID, signal) {
|
|
6959
7221
|
const session = this.sessions[sessionId];
|
|
6960
7222
|
if (!session)
|
|
6961
7223
|
return;
|
|
7224
|
+
// As in `canUseTool`: the work a cancel stopped shows nothing.
|
|
7225
|
+
const stopped = this.isStoppedWorkToolUse(session, toolUseID, signal);
|
|
7226
|
+
if (stopped instanceof Promise ? await stopped : stopped)
|
|
7227
|
+
return;
|
|
6962
7228
|
const toolInput = (input.tool_input ?? {});
|
|
6963
7229
|
const content = await previewPatchContent(input.tool_name, toolInput, session.cwd, "v2");
|
|
6964
7230
|
if (!content)
|
|
@@ -7000,6 +7266,14 @@ export class ClaudeAcpAgent {
|
|
|
7000
7266
|
message: "Session not found",
|
|
7001
7267
|
};
|
|
7002
7268
|
}
|
|
7269
|
+
// The work a cancel stopped shows the client nothing after it was told
|
|
7270
|
+
// that the work ended: neither a tool call nor a request. Its tool does
|
|
7271
|
+
// not run.
|
|
7272
|
+
const stopped = this.isStoppedWorkToolUse(session, toolUseID, signal);
|
|
7273
|
+
if (stopped instanceof Promise ? await stopped : stopped) {
|
|
7274
|
+
this.logger.log(`Session ${sessionId}: denied ${toolName} ${toolUseID}, which the cancelled work asked for`);
|
|
7275
|
+
return { behavior: "deny", message: "The user cancelled this prompt." };
|
|
7276
|
+
}
|
|
7003
7277
|
const { parentToolUseId, toolCallSessionId: permissionSessionId } = this.toolUseOwner(session, sessionId, agentID);
|
|
7004
7278
|
if (agentID && !parentToolUseId) {
|
|
7005
7279
|
// The attribution rests on task_started.task_id === canUseTool's
|
|
@@ -7152,7 +7426,7 @@ export class ClaudeAcpAgent {
|
|
|
7152
7426
|
return { action: "decline" };
|
|
7153
7427
|
}
|
|
7154
7428
|
try {
|
|
7155
|
-
const response = await this.withPendingUserInput(sessionId, () => this.
|
|
7429
|
+
const response = await this.withPendingUserInput(sessionId, signal, () => this.elicitFromClient(createRequest, signal));
|
|
7156
7430
|
if (signal.aborted) {
|
|
7157
7431
|
return { action: "cancel" };
|
|
7158
7432
|
}
|
|
@@ -7184,7 +7458,7 @@ export class ClaudeAcpAgent {
|
|
|
7184
7458
|
const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
7185
7459
|
let response;
|
|
7186
7460
|
try {
|
|
7187
|
-
response = await this.withChildAwaitingUser(sessionId, requestSessionId, () => this.withPendingUserInput(sessionId, () => this.
|
|
7461
|
+
response = await this.withChildAwaitingUser(sessionId, requestSessionId, signal, () => this.withPendingUserInput(sessionId, signal, () => this.elicitFromClient(createRequest, signal), agentID));
|
|
7188
7462
|
}
|
|
7189
7463
|
catch (error) {
|
|
7190
7464
|
// A cancellation we requested (signal aborted) settles as an aborted tool
|
|
@@ -7224,7 +7498,7 @@ export class ClaudeAcpAgent {
|
|
|
7224
7498
|
}
|
|
7225
7499
|
let response;
|
|
7226
7500
|
try {
|
|
7227
|
-
response = await this.withPendingUserInput(sessionId, () => this.
|
|
7501
|
+
response = await this.withPendingUserInput(sessionId, signal, () => this.elicitFromClient(refusalFallbackToCreateRequest(prompt, sessionId), signal));
|
|
7228
7502
|
}
|
|
7229
7503
|
catch (error) {
|
|
7230
7504
|
// A cancellation we requested (signal aborted) is expected teardown;
|
|
@@ -8114,10 +8388,14 @@ export class ClaudeAcpAgent {
|
|
|
8114
8388
|
// configured. Managed `deniedModels` drops entries the CLI would refuse.
|
|
8115
8389
|
const settingsAvailableModels = settingsManager.getSettings().availableModels;
|
|
8116
8390
|
const settingsModelOverrides = settingsManager.getSettings().modelOverrides;
|
|
8117
|
-
const
|
|
8391
|
+
const settingsAllowedModels = Array.isArray(settingsAvailableModels)
|
|
8118
8392
|
? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels, settingsModelOverrides, settingsManager.getManagedDeniedModels())
|
|
8119
8393
|
: initializationResult.models;
|
|
8120
|
-
|
|
8394
|
+
// Then keep only the models the `providers/set` gateway listed.
|
|
8395
|
+
const allowedModels = resolvedProvider?.listedModels
|
|
8396
|
+
? narrowToGatewayModels(settingsAllowedModels, resolvedProvider.listedModels, resolvedProvider.bedrockRegionPrefix)
|
|
8397
|
+
: settingsAllowedModels;
|
|
8398
|
+
const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, await resumedModelHint, resolvedProvider?.listedModels !== undefined);
|
|
8121
8399
|
timing.phase("models");
|
|
8122
8400
|
// Resolve the current model's capabilities separately from the stable
|
|
8123
8401
|
// permission-mode catalog advertised to ACP clients.
|
|
@@ -8307,6 +8585,21 @@ export class ClaudeAcpAgent {
|
|
|
8307
8585
|
async enqueueProviderUpdate(config) {
|
|
8308
8586
|
const previous = this.providerUpdate?.catch(() => undefined) ?? Promise.resolve();
|
|
8309
8587
|
const update = previous.then(async () => {
|
|
8588
|
+
// The listing runs in the queue, so `providers/set`, `providers/disable`
|
|
8589
|
+
// and logout apply in the order they arrived. A failed listing fails this
|
|
8590
|
+
// `providers/set` before the provider replaces the current one.
|
|
8591
|
+
if (config) {
|
|
8592
|
+
try {
|
|
8593
|
+
Object.assign(config, await this.listProviderModels(config));
|
|
8594
|
+
}
|
|
8595
|
+
catch (error) {
|
|
8596
|
+
if (error instanceof GatewayListingError) {
|
|
8597
|
+
throw RequestError.internalError({ providerId: PROVIDER_ID, apiType: config.apiType, baseUrl: config.baseUrl }, `Model discovery for provider "${PROVIDER_ID}" failed: ${error.message}`);
|
|
8598
|
+
}
|
|
8599
|
+
throw error;
|
|
8600
|
+
}
|
|
8601
|
+
this.logger.log(`[providers/set] listedModels=${config.listedModels?.length ?? "none"} bedrockRegionPrefix=${config.bedrockRegionPrefix ?? "none"}`);
|
|
8602
|
+
}
|
|
8310
8603
|
const sessions = Object.entries(this.sessions);
|
|
8311
8604
|
const activeTurns = sessions.flatMap(([, session]) => (session.turnQueue ?? []).flatMap((turn) => (turn.completion ? [turn.completion] : [])));
|
|
8312
8605
|
if (activeTurns.length > 0) {
|
|
@@ -8751,9 +9044,12 @@ function createEnvForProvider(config) {
|
|
|
8751
9044
|
return {
|
|
8752
9045
|
...resetRouting,
|
|
8753
9046
|
CLAUDE_CODE_USE_BEDROCK: "1",
|
|
8754
|
-
AWS_BEARER_TOKEN_BEDROCK:
|
|
9047
|
+
AWS_BEARER_TOKEN_BEDROCK: PROVIDER_AUTH_TOKEN_PLACEHOLDER, // Bypass local AWS credential checks
|
|
8755
9048
|
ANTHROPIC_BEDROCK_BASE_URL: config.baseUrl,
|
|
8756
9049
|
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
9050
|
+
...(config.bedrockRegionPrefix
|
|
9051
|
+
? { ANTHROPIC_BEDROCK_REGION_PREFIX: config.bedrockRegionPrefix }
|
|
9052
|
+
: {}),
|
|
8757
9053
|
};
|
|
8758
9054
|
}
|
|
8759
9055
|
if (config.apiType === "vertex") {
|
|
@@ -8778,9 +9074,13 @@ function createEnvForProvider(config) {
|
|
|
8778
9074
|
...resetRouting,
|
|
8779
9075
|
ANTHROPIC_BASE_URL: config.baseUrl,
|
|
8780
9076
|
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
8781
|
-
ANTHROPIC_AUTH_TOKEN:
|
|
9077
|
+
ANTHROPIC_AUTH_TOKEN: PROVIDER_AUTH_TOKEN_PLACEHOLDER, // Bypass local Claude login checks
|
|
8782
9078
|
};
|
|
8783
9079
|
}
|
|
9080
|
+
function sameHeaders(a, b) {
|
|
9081
|
+
const aKeys = Object.keys(a);
|
|
9082
|
+
return aKeys.length === Object.keys(b).length && aKeys.every((key) => b[key] === a[key]);
|
|
9083
|
+
}
|
|
8784
9084
|
/**
|
|
8785
9085
|
* Validate a provider base URL: must be a non-empty absolute http(s) URL.
|
|
8786
9086
|
*/
|
|
@@ -8936,10 +9236,12 @@ export function resolveFastModeEnabled(params) {
|
|
|
8936
9236
|
throw new Error(`Invalid value for config option ${FAST_MODE_CONFIG_ID}: ${value}`);
|
|
8937
9237
|
}
|
|
8938
9238
|
export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel, agents = [], currentAgent = DEFAULT_AGENT_ID, fastMode, presentation) {
|
|
8939
|
-
const options = [
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
9239
|
+
const options = [SessionModeManager.configOption(modes)];
|
|
9240
|
+
// A gateway that lists none of the SDK's models leaves no model to offer, so
|
|
9241
|
+
// there is no model option at all.
|
|
9242
|
+
if (models.availableModels.length > 0) {
|
|
9243
|
+
options.push(buildModelConfigOption(models, modelInfos, presentation?.useRecommendedValue === true));
|
|
9244
|
+
}
|
|
8943
9245
|
const effort = buildEffortConfigOption(modelInfos, models.currentModelId, currentEffortLevel, presentation?.useRecommendedValue === true);
|
|
8944
9246
|
if (effort)
|
|
8945
9247
|
options.push(effort);
|
|
@@ -9217,14 +9519,38 @@ function recordDispatchedToolUses(session, content) {
|
|
|
9217
9519
|
if (!Array.isArray(content))
|
|
9218
9520
|
return;
|
|
9219
9521
|
for (const block of content) {
|
|
9220
|
-
if ((block
|
|
9221
|
-
block?.type === "server_tool_use" ||
|
|
9222
|
-
block?.type === "mcp_tool_use") &&
|
|
9223
|
-
typeof block.id === "string") {
|
|
9522
|
+
if (isToolUseBlock(block))
|
|
9224
9523
|
recordDispatchedToolCall(session, block.id);
|
|
9225
|
-
}
|
|
9226
9524
|
}
|
|
9227
9525
|
}
|
|
9526
|
+
function isToolUseBlock(block) {
|
|
9527
|
+
const { type, id } = (block ?? {});
|
|
9528
|
+
return ((type === "tool_use" || type === "server_tool_use" || type === "mcp_tool_use") &&
|
|
9529
|
+
typeof id === "string");
|
|
9530
|
+
}
|
|
9531
|
+
const MAX_REACHED_TOOL_USES = 1000;
|
|
9532
|
+
/**
|
|
9533
|
+
* Records that the consumer reached the tool use `toolUseId`, and whether it
|
|
9534
|
+
* dropped it as output of the work a cancel stopped, then wakes what waits for
|
|
9535
|
+
* it (see `isStoppedWorkToolUse`). The first record of a tool use stays.
|
|
9536
|
+
*/
|
|
9537
|
+
function recordReachedToolUse(session, toolUseId, dropped) {
|
|
9538
|
+
const reached = (session.reachedToolUses ??= new Map());
|
|
9539
|
+
if (reached.has(toolUseId))
|
|
9540
|
+
return;
|
|
9541
|
+
reached.set(toolUseId, dropped);
|
|
9542
|
+
if (reached.size > MAX_REACHED_TOOL_USES) {
|
|
9543
|
+
const oldest = reached.keys().next().value;
|
|
9544
|
+
if (oldest !== undefined)
|
|
9545
|
+
reached.delete(oldest);
|
|
9546
|
+
}
|
|
9547
|
+
const waiters = session.toolUseWaiters?.get(toolUseId);
|
|
9548
|
+
if (!waiters)
|
|
9549
|
+
return;
|
|
9550
|
+
session.toolUseWaiters?.delete(toolUseId);
|
|
9551
|
+
for (const wake of waiters)
|
|
9552
|
+
wake();
|
|
9553
|
+
}
|
|
9228
9554
|
/** Streamed and permission-surfaced tools can precede the SDK's user echo. */
|
|
9229
9555
|
function recordForegroundToolCall(session, toolCallId) {
|
|
9230
9556
|
const turn = session.activeTurn ?? session.turnQueue?.find((queued) => !queued.settled);
|