@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/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. A clear-context restart replaces the Claude Code session
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
- if (this.providerUpdate)
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
- if (this.providerUpdate)
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
- if (this.providerUpdate)
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
- if (this.providerUpdate)
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
- const hadRoutingOverride = this.resolveProviderConfig() !== null;
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
- if (this.providerUpdate)
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
- /** Closes the tool calls that the work a cancel stopped left open, as that
3170
- * work ends. Claude Code's interrupt ends each running tool of the main
3171
- * conversation with an error result, which `sendInterruptedToolResults`
3172
- * delivers. What is still open here has no result to deliver:
3173
- * - a tool of a subagent: Claude Code writes its interrupted result only to
3174
- * the subagent's own transcript, not to the stream;
3175
- * - a tool call that Claude abandoned before it ran;
3176
- * - every tool call, when the stream gives no results at all, as when the
3177
- * force-cancel backstop ends a wedged turn.
3178
- * A foreground subagent ends with the work that waits on it. A background
3179
- * subagent keeps its tool calls open until the SDK reports its end, which
3180
- * Claude Code 2.1.293 does for the ones it stops, before the turn's result
3181
- * (see `NativeSubagentRuntime.parentCancelled`).
3182
- * Runs once per turn: a second call, from the other settling path, waits
3183
- * for the first (see `Turn.closingCancelledToolCalls`). */
3184
- const closeCancelledToolCalls = (turn) => (turn.closingCancelledToolCalls ??= (async () => {
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 (parentToolUseId !== undefined && runsInBackground(parentToolUseId))
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
- if (session.cancelled && this.workEndReported(params.sessionId, session)) {
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
- withChildAwaitingUser(ownerSessionId, requestSessionId, request) {
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
- async withPendingUserInput(sessionId, request, agentID) {
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
- this.syncAwaitingUser(sessionId, session);
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
- syncAwaitingUser(sessionId, session) {
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 (!this.foregroundEvents || this.foregroundWork.has(sessionId) || session.cancelled)
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 ? !this.foregroundWork.has(sessionId) : !session.activeTurn;
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.client.createElicitation(createRequest, signal));
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.client.createElicitation(createRequest, signal), agentID));
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.client.createElicitation(refusalFallbackToCreateRequest(prompt, sessionId), signal));
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 allowedModels = Array.isArray(settingsAvailableModels)
8391
+ const settingsAllowedModels = Array.isArray(settingsAvailableModels)
8118
8392
  ? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels, settingsModelOverrides, settingsManager.getManagedDeniedModels())
8119
8393
  : initializationResult.models;
8120
- const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, await resumedModelHint);
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: "acp-proxy", // Bypass local AWS credential checks
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: "acp-proxy", // Bypass local Claude login checks
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
- SessionModeManager.configOption(modes),
8941
- buildModelConfigOption(models, modelInfos, presentation?.useRecommendedValue === true),
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?.type === "tool_use" ||
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);