@agentclientprotocol/claude-agent-acp 0.90.1-preview.9 → 0.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +14 -5
  2. package/dist/acp-agent.d.ts +91 -11
  3. package/dist/acp-agent.d.ts.map +1 -1
  4. package/dist/acp-agent.js +244 -75
  5. package/dist/air-extension.d.ts +6 -0
  6. package/dist/air-extension.d.ts.map +1 -1
  7. package/dist/air-extension.js +6 -0
  8. package/dist/gateway-models.d.ts +62 -0
  9. package/dist/gateway-models.d.ts.map +1 -0
  10. package/dist/gateway-models.js +180 -0
  11. package/dist/native-subagents.d.ts +34 -9
  12. package/dist/native-subagents.d.ts.map +1 -1
  13. package/dist/native-subagents.js +55 -11
  14. package/dist/session-index/connection.d.ts +4 -3
  15. package/dist/session-index/connection.d.ts.map +1 -1
  16. package/dist/session-index/connection.js +8 -7
  17. package/dist/session-index/list-subscriptions.d.ts.map +1 -1
  18. package/dist/session-index/list-subscriptions.js +4 -1
  19. package/dist/session-index/service.d.ts +10 -0
  20. package/dist/session-index/service.d.ts.map +1 -1
  21. package/dist/session-index/service.js +21 -0
  22. package/dist/session-index/session-index.d.ts +10 -2
  23. package/dist/session-index/session-index.d.ts.map +1 -1
  24. package/dist/session-index/session-index.js +21 -5
  25. package/dist/session-index/session-info.d.ts.map +1 -1
  26. package/dist/session-index/session-info.js +1 -0
  27. package/dist/session-index/transcript-scan.d.ts +25 -1
  28. package/dist/session-index/transcript-scan.d.ts.map +1 -1
  29. package/dist/session-index/transcript-scan.js +57 -1
  30. package/dist/session-index/worktrees.d.ts +6 -0
  31. package/dist/session-index/worktrees.d.ts.map +1 -1
  32. package/dist/session-index/worktrees.js +12 -0
  33. package/dist/session-model.d.ts +21 -1
  34. package/dist/session-model.d.ts.map +1 -1
  35. package/dist/session-model.js +89 -11
  36. package/dist/turn-events.d.ts +5 -1
  37. package/dist/turn-events.d.ts.map +1 -1
  38. package/dist/v2/prompt.d.ts +2 -1
  39. package/dist/v2/prompt.d.ts.map +1 -1
  40. package/dist/v2/prompt.js +2 -1
  41. package/package.json +3 -3
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";
@@ -1437,7 +1438,7 @@ export class ClaudeAcpAgent {
1437
1438
  };
1438
1439
  }
1439
1440
  async newSession(params) {
1440
- if (this.providerUpdate)
1441
+ while (this.providerUpdate)
1441
1442
  await this.providerUpdate;
1442
1443
  const response = await this.createSession(params, {
1443
1444
  // Revisit these meta values once we support resume
@@ -1504,7 +1505,7 @@ export class ClaudeAcpAgent {
1504
1505
  * later load or resume with the same settings reuses it.
1505
1506
  */
1506
1507
  async unstable_forkSession(params) {
1507
- if (this.providerUpdate)
1508
+ while (this.providerUpdate)
1508
1509
  await this.providerUpdate;
1509
1510
  const { sessionId } = await forkSession(params, {
1510
1511
  liveMessageIdToUuid: this.sessions[params.sessionId]?.messageIdToUuid,
@@ -1522,7 +1523,7 @@ export class ClaudeAcpAgent {
1522
1523
  return { sessionId, modes, configOptions };
1523
1524
  }
1524
1525
  async resumeSession(params) {
1525
- if (this.providerUpdate)
1526
+ while (this.providerUpdate)
1526
1527
  await this.providerUpdate;
1527
1528
  const result = await this.getOrCreateSession(params);
1528
1529
  this.afterSetupResponse(params.sessionId, params.mcpServers ?? []);
@@ -1530,7 +1531,7 @@ export class ClaudeAcpAgent {
1530
1531
  }
1531
1532
  async loadSession(params) {
1532
1533
  const timing = new SessionTiming(this.logger, "load", params.sessionId);
1533
- if (this.providerUpdate)
1534
+ while (this.providerUpdate)
1534
1535
  await this.providerUpdate;
1535
1536
  let result;
1536
1537
  if (this.sessions[params.sessionId]) {
@@ -1912,6 +1913,42 @@ export class ClaudeAcpAgent {
1912
1913
  // Unknown provider: idempotent success.
1913
1914
  return {};
1914
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
+ }
1915
1952
  resolveProviderConfig() {
1916
1953
  return this.providerConfig ?? gatewayRequestToProviderConfig(this.gatewayAuthRequest);
1917
1954
  }
@@ -1945,7 +1982,9 @@ export class ClaudeAcpAgent {
1945
1982
  // provider routing set via `providers/set`. Neither touches the on-disk
1946
1983
  // credential store, so dropping these references is the whole logout for
1947
1984
  // those paths.
1948
- 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;
1949
1988
  this.gatewayAuthRequest = undefined;
1950
1989
  this.providerConfig = undefined;
1951
1990
  // Any probe already running read the pre-logout world; the bump makes its
@@ -2050,7 +2089,7 @@ export class ClaudeAcpAgent {
2050
2089
  * before this resolves.
2051
2090
  */
2052
2091
  async startTurn(params, events) {
2053
- if (this.providerUpdate)
2092
+ while (this.providerUpdate)
2054
2093
  await this.providerUpdate;
2055
2094
  let session = this.sessions[params.sessionId];
2056
2095
  if (!session) {
@@ -2227,34 +2266,13 @@ export class ClaudeAcpAgent {
2227
2266
  // husk that reached this by another route still releases its resources.
2228
2267
  this.closeQueryStream(session);
2229
2268
  try {
2230
- // `resume` names the Claude session, which shares the ACP session id, so
2231
- // the new query continues the same conversation under the same id.
2232
- await this.createSession(creationParams, {
2233
- resume: sessionId,
2269
+ // The very first turn may be the one that signed out, so the CLI may
2270
+ // never have written the conversation.
2271
+ await this.recreateLiveSession(creationParams, sessionId, {
2234
2272
  permissionMode: session.modes.currentModeId,
2235
2273
  });
2236
2274
  }
2237
2275
  catch (error) {
2238
- if (error instanceof RequestError && error.code === RequestError.resourceNotFound().code) {
2239
- // The CLI never wrote this conversation, because the very first turn
2240
- // was the one that signed out. Resuming it fails for ever, so start a
2241
- // fresh query under the same ACP session id: an empty history is a far
2242
- // better answer than a session the client can never use again.
2243
- this.logger.log(`session ${sessionId} was never persisted; starting a fresh query under the same id`);
2244
- try {
2245
- await this.createSession(creationParams, {
2246
- reuseSessionId: sessionId,
2247
- permissionMode: session.modes.currentModeId,
2248
- });
2249
- return;
2250
- }
2251
- catch (fallbackError) {
2252
- if (!this.sessions[sessionId]) {
2253
- this.sessions[sessionId] = session;
2254
- }
2255
- throw fallbackError;
2256
- }
2257
- }
2258
2276
  // Keep the husk addressable: the client answers the refusal with its own
2259
2277
  // auth flow and then retries this session.
2260
2278
  if (!this.sessions[sessionId]) {
@@ -2263,6 +2281,31 @@ export class ClaudeAcpAgent {
2263
2281
  throw error;
2264
2282
  }
2265
2283
  }
2284
+ /**
2285
+ * Starts a new query for the session `sessionId`, which ran on this
2286
+ * connection and whose query is gone: it resumes the stored conversation.
2287
+ * `resume` names the Claude session, which shares the ACP session id, so the
2288
+ * new query continues the same conversation under the same id. Claude Code
2289
+ * writes the conversation at the first prompt, so resuming a session that was
2290
+ * never prompted fails with `resourceNotFound`, and keeps failing. Such a
2291
+ * session starts a fresh query under the same id instead: it has no history
2292
+ * to lose, and the client keeps a session it can use.
2293
+ */
2294
+ async recreateLiveSession(params, sessionId, options) {
2295
+ try {
2296
+ return await this.createSession(params, { resume: sessionId, ...options });
2297
+ }
2298
+ catch (error) {
2299
+ if (!(error instanceof RequestError && error.code === RequestError.resourceNotFound().code)) {
2300
+ throw error;
2301
+ }
2302
+ this.logger.log(`session ${sessionId} was never persisted; starting a fresh query under the same id`);
2303
+ return await this.createSession(params, {
2304
+ reuseSessionId: sessionId,
2305
+ permissionMode: options.permissionMode,
2306
+ });
2307
+ }
2308
+ }
2266
2309
  /** Run the `--hide-claude-auth` guard before a turn starts. The returned
2267
2310
  * promise rejects with the `authRequired` error when a claude.ai
2268
2311
  * subscription would pay, or when the account holds no credential this
@@ -3055,6 +3098,7 @@ export class ClaudeAcpAgent {
3055
3098
  session.liveBackgroundTasks.set(taskId, {
3056
3099
  parentToolUseId,
3057
3100
  isSubagent,
3101
+ ...(placement.taskType !== undefined ? { taskType: placement.taskType } : {}),
3058
3102
  ...(placement.backgrounded !== undefined ? { backgrounded: placement.backgrounded } : {}),
3059
3103
  ...(placement.parentTaskId !== undefined ? { parentTaskId: placement.parentTaskId } : {}),
3060
3104
  });
@@ -3295,27 +3339,46 @@ export class ClaudeAcpAgent {
3295
3339
  await sendUpdate(notification);
3296
3340
  }
3297
3341
  };
3298
- /** A cancel stopped the model cycle that Claude Code started on its own, and
3299
- * its end is about to be reported: closes the tool calls that it left open
3300
- * first. A cycle is foreground work that no turn settles, so
3301
- * `closeCancelledToolCalls` does not run for it. Claude Code's interrupt
3302
- * gives the tools of the main conversation results, which
3303
- * `sendInterruptedToolResults` delivers while the cycle runs. What is still
3304
- * open has none, such as the command of a foreground subagent. A turn that
3305
- * took the cycle over closes its own calls, when it settles.
3306
- * The follow-up of a turn that v1 holds for its background subagents is a
3307
- * cycle too, and the cancel ends the held turn as well, possibly in
3308
- * `cancelTurns` while these reports are in flight. So the cycle's calls
3309
- * close with the held turn's closing, which each settle of the turn waits
3310
- * for, and the response never comes before them. */
3311
- const closeCancelledCycleToolCalls = async () => {
3312
- if (!session.cancelled || !this.runsAutonomousCycle(params.sessionId))
3342
+ /** The model cycle that Claude Code started on its own ends, and its end is
3343
+ * about to be reported: closes the tool calls that it left open first. A
3344
+ * cycle is foreground work that no turn settles, so neither
3345
+ * `closeCancelledToolCalls` nor the check of `settleActive` for unfinished
3346
+ * tools runs for it. A turn that took the cycle over closes its own calls,
3347
+ * when it settles.
3348
+ *
3349
+ * After a cancel, Claude Code's interrupt gives the tools of the main
3350
+ * conversation results, which `sendInterruptedToolResults` delivers while
3351
+ * the cycle runs. What is still open has none, such as the command of a
3352
+ * foreground subagent. The follow-up of a turn that v1 holds for its
3353
+ * background subagents is a cycle too, and the cancel ends the held turn
3354
+ * as well, possibly in `cancelTurns` while these reports are in flight. So
3355
+ * the cycle's calls close with the held turn's closing, which each settle
3356
+ * of the turn waits for, and the response never comes before them.
3357
+ *
3358
+ * At any other end, every call that ran has its result. What is open is a
3359
+ * call of the main conversation that Claude abandoned before it ran, such
3360
+ * as a streamed tool_use that never reached a complete message, which a
3361
+ * turn's end closes the same way (`closeUnfinished`). The calls of a
3362
+ * subagent are not the cycle's: a background subagent runs on. */
3363
+ const closeCycleToolCalls = async () => {
3364
+ if (!this.runsAutonomousCycle(params.sessionId))
3365
+ return;
3366
+ if (session.cancelled) {
3367
+ const held = session.activeTurn;
3368
+ if (held && !held.settled)
3369
+ await closeCancelledToolCalls(held);
3370
+ else
3371
+ await closeOpenToolCalls(closedAtCancel);
3313
3372
  return;
3314
- const held = session.activeTurn;
3315
- if (held && !held.settled)
3316
- await closeCancelledToolCalls(held);
3317
- else
3318
- await closeOpenToolCalls(closedAtCancel);
3373
+ }
3374
+ const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
3375
+ for (const toolCallId of [...session.emittedToolCalls]) {
3376
+ if (session.toolCallParents?.has(toolCallId))
3377
+ continue;
3378
+ if (session.dispatchedToolCalls?.has(toolCallId))
3379
+ continue;
3380
+ await closeToolCall(toolCallId, renderer.unfinished({ toolCallId, ran: false }));
3381
+ }
3319
3382
  };
3320
3383
  /** At the actual turn boundary, preview its checkpoint, settle the active
3321
3384
  * turn exactly once, disarm the force-cancel backstop, and drop it from
@@ -3966,7 +4029,7 @@ export class ClaudeAcpAgent {
3966
4029
  // here. An owed idle belongs to an earlier result: it can lag
3967
4030
  // past the cycle's first output.
3968
4031
  if (session.owedTrailingIdles === 0) {
3969
- await closeCancelledCycleToolCalls();
4032
+ await closeCycleToolCalls();
3970
4033
  this.endAutonomousCycle(params.sessionId, {
3971
4034
  outcome: { stopReason: session.cancelled ? "cancelled" : "end_turn" },
3972
4035
  });
@@ -4271,6 +4334,7 @@ export class ClaudeAcpAgent {
4271
4334
  registerLiveTask(message.task_id, message.tool_use_id, !!message.subagent_type, {
4272
4335
  backgrounded: message.is_backgrounded,
4273
4336
  parentTaskId: message.parent_task_id,
4337
+ taskType: message.task_type,
4274
4338
  });
4275
4339
  await subagents.taskStarted({
4276
4340
  taskId: message.task_id,
@@ -4789,7 +4853,7 @@ export class ClaudeAcpAgent {
4789
4853
  pendingExitPlanContextReset?.toolUseId ===
4790
4854
  pendingExitPlanModeInterruption?.toolUseId;
4791
4855
  if (!continuesPlan && !(isAutonomousResult && message.num_turns === 0)) {
4792
- await closeCancelledCycleToolCalls();
4856
+ await closeCycleToolCalls();
4793
4857
  this.endAutonomousCycle(params.sessionId, autonomousCycleEnd(message, resultModelUsage, session.cancelled || exitPlanInterruption || isAbortedResult(message)));
4794
4858
  }
4795
4859
  if (session.cancelled) {
@@ -5875,8 +5939,46 @@ export class ClaudeAcpAgent {
5875
5939
  }
5876
5940
  }
5877
5941
  async cancel(params) {
5942
+ if (!this.sessions[params.sessionId] && (await this.cancelChildSession(params.sessionId))) {
5943
+ return;
5944
+ }
5878
5945
  await this.cancelTurns(params, { awaitInterrupt: true });
5879
5946
  }
5947
+ /**
5948
+ * Cancels the current work of the subagent child session `sessionId`, which
5949
+ * its `cancel` capability permits on the subagents RFD form, without
5950
+ * cancelling its parent (see `NativeSubagentRuntime.cancelChild`). Returns
5951
+ * false when no session has such a child.
5952
+ */
5953
+ async cancelChildSession(sessionId) {
5954
+ for (const [rootSessionId, session] of Object.entries(this.sessions)) {
5955
+ const cancelled = await session.nativeSubagentRuntime?.cancelChild(sessionId, (taskId) => session.query.stopTask(taskId).catch((error) => {
5956
+ this.logger.error(`Session ${rootSessionId}: failed to stop the task ${taskId} of subagent session ${sessionId}`, error);
5957
+ }));
5958
+ if (cancelled)
5959
+ return true;
5960
+ }
5961
+ return false;
5962
+ }
5963
+ /**
5964
+ * Stops each background workflow of the session that still runs, after the
5965
+ * interrupt of a `session/cancel` spared it (see
5966
+ * `Session.stopsWorkflowsOnCancel`). Claude Code reports each one
5967
+ * `stopped`, along with the commands inside it, and starts no model cycle
5968
+ * for it, as when the interrupt killed it. Every background workflow is
5969
+ * stopped, whoever launched it, because the interrupt killed them all.
5970
+ */
5971
+ stopBackgroundWorkflows(sessionId, session) {
5972
+ if (!session.stopsWorkflowsOnCancel)
5973
+ return;
5974
+ for (const [taskId, task] of session.liveBackgroundTasks) {
5975
+ if (task.taskType !== "local_workflow")
5976
+ continue;
5977
+ session.query.stopTask(taskId).catch((error) => {
5978
+ this.logger.error(`Session ${sessionId}: failed to stop the background workflow ${taskId}`, error);
5979
+ });
5980
+ }
5981
+ }
5880
5982
  /** Cancel the session's turns and interrupt the SDK query. With
5881
5983
  * `awaitInterrupt: false` the interrupt is sent, but its reply is not
5882
5984
  * awaited. `teardownSession` uses that: it closes the query right after, and
@@ -5889,6 +5991,10 @@ export class ClaudeAcpAgent {
5889
5991
  return;
5890
5992
  }
5891
5993
  session.cancelled = true;
5994
+ // The interrupt below makes Claude Code withdraw the requests that are open
5995
+ // now (see `withPendingUserInput`).
5996
+ for (const input of session.openUserInputs ?? [])
5997
+ input.cancelled = true;
5892
5998
  // Before anything awaits, so a model request that the consumer reads in
5893
5999
  // the meantime is already judged against these prompts.
5894
6000
  session.cancelledPrompts ??= new Set();
@@ -6157,6 +6263,7 @@ export class ClaudeAcpAgent {
6157
6263
  Promise.resolve(interrupt).catch(() => { });
6158
6264
  return;
6159
6265
  }
6266
+ this.stopBackgroundWorkflows(params.sessionId, session);
6160
6267
  const receipt = await interrupt;
6161
6268
  // On CLIs advertising `interrupt_receipt_v1`, the receipt's `still_queued`
6162
6269
  // lists exactly which queued messages survive the interrupt and will still
@@ -6829,8 +6936,17 @@ export class ClaudeAcpAgent {
6829
6936
  * conversation) as awaiting user input for exactly the lifetime of its
6830
6937
  * promise (see `Session.openUserInputs`). Steering consults the open
6831
6938
  * requests synchronously, so a message arriving while any permission or
6832
- * elicitation card is open uses non-interrupting SDK delivery. */
6833
- async withPendingUserInput(sessionId, request, agentID) {
6939
+ * elicitation card is open uses non-interrupting SDK delivery.
6940
+ *
6941
+ * `signal` aborts when Claude Code withdraws the request, which is not an
6942
+ * answer. Example: the user cancels while a permission request is open.
6943
+ * Claude Code's interrupt withdraws it as it stops the work, and the work
6944
+ * ends `idle cancelled` right after. The end of the request then reports
6945
+ * no `running`: the client would see `requires_action`, `running`, `idle
6946
+ * cancelled`. A withdrawal without a cancel means that something else
6947
+ * answered the request in the user's place, such as a `PermissionRequest`
6948
+ * hook, and the work goes on, so the end reports `running`. */
6949
+ async withPendingUserInput(sessionId, signal, request, agentID) {
6834
6950
  const session = this.sessions[sessionId];
6835
6951
  if (!session)
6836
6952
  return request();
@@ -6842,7 +6958,8 @@ export class ClaudeAcpAgent {
6842
6958
  }
6843
6959
  finally {
6844
6960
  session.openUserInputs.delete(input);
6845
- this.syncAwaitingUser(sessionId, session);
6961
+ const withdrawnByCancel = signal.aborted && input.cancelled === true;
6962
+ this.syncAwaitingUser(sessionId, session, withdrawnByCancel);
6846
6963
  }
6847
6964
  }
6848
6965
  /** Report whether the session's foreground work awaits the user: exactly
@@ -6851,8 +6968,13 @@ export class ClaudeAcpAgent {
6851
6968
  * and Claude Code can ask for a queued prompt before the consumer activates
6852
6969
  * it, so a request is not tied to the work that was running when it
6853
6970
  * opened. Called when a request opens or closes, when a turn activates, and
6854
- * when a task registers or moves to the background. */
6855
- syncAwaitingUser(sessionId, session) {
6971
+ * when a task registers or moves to the background.
6972
+ *
6973
+ * When the last request ends, the work reports `running` again, unless a
6974
+ * cancel withdrew that request (`withdrawnByCancel`, see
6975
+ * {@link withPendingUserInput}): the work is being stopped, and reports its
6976
+ * end next. */
6977
+ syncAwaitingUser(sessionId, session, withdrawnByCancel = false) {
6856
6978
  const work = this.foregroundWork.get(sessionId);
6857
6979
  if (!work)
6858
6980
  return;
@@ -6862,7 +6984,7 @@ export class ClaudeAcpAgent {
6862
6984
  work.awaitingUser = waiting;
6863
6985
  if (waiting)
6864
6986
  work.events.awaitingUser();
6865
- else
6987
+ else if (!withdrawnByCancel)
6866
6988
  work.events.running();
6867
6989
  }
6868
6990
  /** Make `owner` the foreground work of the session `sessionId` (see
@@ -7017,7 +7139,7 @@ export class ClaudeAcpAgent {
7017
7139
  // The request goes to a subagent's child session for a tool call of that
7018
7140
  // subagent, but it blocks the turn of the owner session.
7019
7141
  try {
7020
- return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, signal, () => this.withPendingUserInput(ownerSessionId, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID));
7142
+ return await this.withChildAwaitingUser(ownerSessionId, params.sessionId, signal, () => this.withPendingUserInput(ownerSessionId, signal, () => raceWithAbort(this.client.requestPermission(params, signal), signal), agentID));
7021
7143
  }
7022
7144
  catch (error) {
7023
7145
  if (signal.aborted) {
@@ -7349,7 +7471,7 @@ export class ClaudeAcpAgent {
7349
7471
  return { action: "decline" };
7350
7472
  }
7351
7473
  try {
7352
- const response = await this.withPendingUserInput(sessionId, () => this.elicitFromClient(createRequest, signal));
7474
+ const response = await this.withPendingUserInput(sessionId, signal, () => this.elicitFromClient(createRequest, signal));
7353
7475
  if (signal.aborted) {
7354
7476
  return { action: "cancel" };
7355
7477
  }
@@ -7381,7 +7503,7 @@ export class ClaudeAcpAgent {
7381
7503
  const createRequest = askUserQuestionsToCreateRequest(questions, requestSessionId, toolUseID, this.toolCallCapabilities.air.client);
7382
7504
  let response;
7383
7505
  try {
7384
- response = await this.withChildAwaitingUser(sessionId, requestSessionId, signal, () => this.withPendingUserInput(sessionId, () => this.elicitFromClient(createRequest, signal), agentID));
7506
+ response = await this.withChildAwaitingUser(sessionId, requestSessionId, signal, () => this.withPendingUserInput(sessionId, signal, () => this.elicitFromClient(createRequest, signal), agentID));
7385
7507
  }
7386
7508
  catch (error) {
7387
7509
  // A cancellation we requested (signal aborted) settles as an aborted tool
@@ -7421,7 +7543,7 @@ export class ClaudeAcpAgent {
7421
7543
  }
7422
7544
  let response;
7423
7545
  try {
7424
- response = await this.withPendingUserInput(sessionId, () => this.elicitFromClient(refusalFallbackToCreateRequest(prompt, sessionId), signal));
7546
+ response = await this.withPendingUserInput(sessionId, signal, () => this.elicitFromClient(refusalFallbackToCreateRequest(prompt, sessionId), signal));
7425
7547
  }
7426
7548
  catch (error) {
7427
7549
  // A cancellation we requested (signal aborted) is expected teardown;
@@ -7826,16 +7948,22 @@ export class ClaudeAcpAgent {
7826
7948
  // up the new values.
7827
7949
  await this.teardownSession(params.sessionId);
7828
7950
  }
7829
- const response = await this.createSession({
7951
+ const sessionParams = {
7830
7952
  cwd: params.cwd,
7831
7953
  mcpServers: params.mcpServers ?? [],
7832
7954
  additionalDirectories: params.additionalDirectories,
7833
7955
  _meta: params._meta,
7834
- }, {
7835
- resume: params.sessionId,
7956
+ };
7957
+ const creation = {
7836
7958
  permissionMode: livePermissionMode,
7837
7959
  ...(resumedSession ? { resumedModelHint: resumedSession.model } : {}),
7838
- });
7960
+ };
7961
+ // A live session may never have been prompted, so it may have no stored
7962
+ // conversation to resume (see `recreateLiveSession`). For a session that
7963
+ // is not loaded here, a missing conversation means an unknown session.
7964
+ const response = existingSession
7965
+ ? await this.recreateLiveSession(sessionParams, params.sessionId, creation)
7966
+ : await this.createSession(sessionParams, { resume: params.sessionId, ...creation });
7839
7967
  return {
7840
7968
  sessionId: response.sessionId,
7841
7969
  modes: response.modes,
@@ -8081,6 +8209,17 @@ export class ClaudeAcpAgent {
8081
8209
  systemPrompt,
8082
8210
  settingSources: ["user", "project", "local"],
8083
8211
  ...(thinking !== undefined && { thinking }),
8212
+ // A client on the subagents RFD form can stop one subagent with
8213
+ // `session/cancel` on its child session (see
8214
+ // `NativeSubagentRuntime.cancelChild`). Declaring that makes Claude
8215
+ // Code's interrupt spare background subagents, so they keep running
8216
+ // after a parent cancel, as the RFD prescribes
8217
+ // (agentclientprotocol/agent-client-protocol#2308). Without it, the
8218
+ // interrupt kills them. It spares background workflows too, which
8219
+ // `stopBackgroundWorkflows` stops instead. Background commands survive an
8220
+ // interrupt either way. Before `userProvidedOptions`, so a client's own
8221
+ // `perTaskStopAffordance` wins.
8222
+ ...(subagentForm(this.clientCapabilities) === "rfd" ? { perTaskStopAffordance: true } : {}),
8084
8223
  ...userProvidedOptions,
8085
8224
  // Claude Code uses the same checkpoint store for /rewind. Enable it only
8086
8225
  // for clients that negotiated per-turn file-change reports; this avoids
@@ -8311,10 +8450,14 @@ export class ClaudeAcpAgent {
8311
8450
  // configured. Managed `deniedModels` drops entries the CLI would refuse.
8312
8451
  const settingsAvailableModels = settingsManager.getSettings().availableModels;
8313
8452
  const settingsModelOverrides = settingsManager.getSettings().modelOverrides;
8314
- const allowedModels = Array.isArray(settingsAvailableModels)
8453
+ const settingsAllowedModels = Array.isArray(settingsAvailableModels)
8315
8454
  ? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels, settingsModelOverrides, settingsManager.getManagedDeniedModels())
8316
8455
  : initializationResult.models;
8317
- const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, await resumedModelHint);
8456
+ // Then keep only the models the `providers/set` gateway listed.
8457
+ const allowedModels = resolvedProvider?.listedModels
8458
+ ? narrowToGatewayModels(settingsAllowedModels, resolvedProvider.listedModels, resolvedProvider.bedrockRegionPrefix)
8459
+ : settingsAllowedModels;
8460
+ const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger, creationOpts.resume !== undefined, sessionId, await resumedModelHint, resolvedProvider?.listedModels !== undefined);
8318
8461
  timing.phase("models");
8319
8462
  // Resolve the current model's capabilities separately from the stable
8320
8463
  // permission-mode catalog advertised to ACP clients.
@@ -8472,6 +8615,8 @@ export class ClaudeAcpAgent {
8472
8615
  emittedToolCalls: new Set(),
8473
8616
  eagerToolCallSessions: new Map(),
8474
8617
  liveBackgroundTasks: new Map(),
8618
+ stopsWorkflowsOnCancel: options.perTaskStopAffordance === true &&
8619
+ !clientSupportsAsyncTasks(this.clientCapabilities),
8475
8620
  nativeSubagentsByTaskId: new Map(),
8476
8621
  nativeSubagentTaskIdByToolUseId: new Map(),
8477
8622
  nativeSubagentParentByToolUseId: new Map(),
@@ -8504,6 +8649,21 @@ export class ClaudeAcpAgent {
8504
8649
  async enqueueProviderUpdate(config) {
8505
8650
  const previous = this.providerUpdate?.catch(() => undefined) ?? Promise.resolve();
8506
8651
  const update = previous.then(async () => {
8652
+ // The listing runs in the queue, so `providers/set`, `providers/disable`
8653
+ // and logout apply in the order they arrived. A failed listing fails this
8654
+ // `providers/set` before the provider replaces the current one.
8655
+ if (config) {
8656
+ try {
8657
+ Object.assign(config, await this.listProviderModels(config));
8658
+ }
8659
+ catch (error) {
8660
+ if (error instanceof GatewayListingError) {
8661
+ throw RequestError.internalError({ providerId: PROVIDER_ID, apiType: config.apiType, baseUrl: config.baseUrl }, `Model discovery for provider "${PROVIDER_ID}" failed: ${error.message}`);
8662
+ }
8663
+ throw error;
8664
+ }
8665
+ this.logger.log(`[providers/set] listedModels=${config.listedModels?.length ?? "none"} bedrockRegionPrefix=${config.bedrockRegionPrefix ?? "none"}`);
8666
+ }
8507
8667
  const sessions = Object.entries(this.sessions);
8508
8668
  const activeTurns = sessions.flatMap(([, session]) => (session.turnQueue ?? []).flatMap((turn) => (turn.completion ? [turn.completion] : [])));
8509
8669
  if (activeTurns.length > 0) {
@@ -8948,9 +9108,12 @@ function createEnvForProvider(config) {
8948
9108
  return {
8949
9109
  ...resetRouting,
8950
9110
  CLAUDE_CODE_USE_BEDROCK: "1",
8951
- AWS_BEARER_TOKEN_BEDROCK: "acp-proxy", // Bypass local AWS credential checks
9111
+ AWS_BEARER_TOKEN_BEDROCK: PROVIDER_AUTH_TOKEN_PLACEHOLDER, // Bypass local AWS credential checks
8952
9112
  ANTHROPIC_BEDROCK_BASE_URL: config.baseUrl,
8953
9113
  ANTHROPIC_CUSTOM_HEADERS: customHeaders,
9114
+ ...(config.bedrockRegionPrefix
9115
+ ? { ANTHROPIC_BEDROCK_REGION_PREFIX: config.bedrockRegionPrefix }
9116
+ : {}),
8954
9117
  };
8955
9118
  }
8956
9119
  if (config.apiType === "vertex") {
@@ -8975,9 +9138,13 @@ function createEnvForProvider(config) {
8975
9138
  ...resetRouting,
8976
9139
  ANTHROPIC_BASE_URL: config.baseUrl,
8977
9140
  ANTHROPIC_CUSTOM_HEADERS: customHeaders,
8978
- ANTHROPIC_AUTH_TOKEN: "acp-proxy", // Bypass local Claude login checks
9141
+ ANTHROPIC_AUTH_TOKEN: PROVIDER_AUTH_TOKEN_PLACEHOLDER, // Bypass local Claude login checks
8979
9142
  };
8980
9143
  }
9144
+ function sameHeaders(a, b) {
9145
+ const aKeys = Object.keys(a);
9146
+ return aKeys.length === Object.keys(b).length && aKeys.every((key) => b[key] === a[key]);
9147
+ }
8981
9148
  /**
8982
9149
  * Validate a provider base URL: must be a non-empty absolute http(s) URL.
8983
9150
  */
@@ -9133,10 +9300,12 @@ export function resolveFastModeEnabled(params) {
9133
9300
  throw new Error(`Invalid value for config option ${FAST_MODE_CONFIG_ID}: ${value}`);
9134
9301
  }
9135
9302
  export function buildConfigOptions(modes, models, modelInfos, currentEffortLevel, agents = [], currentAgent = DEFAULT_AGENT_ID, fastMode, presentation) {
9136
- const options = [
9137
- SessionModeManager.configOption(modes),
9138
- buildModelConfigOption(models, modelInfos, presentation?.useRecommendedValue === true),
9139
- ];
9303
+ const options = [SessionModeManager.configOption(modes)];
9304
+ // A gateway that lists none of the SDK's models leaves no model to offer, so
9305
+ // there is no model option at all.
9306
+ if (models.availableModels.length > 0) {
9307
+ options.push(buildModelConfigOption(models, modelInfos, presentation?.useRecommendedValue === true));
9308
+ }
9140
9309
  const effort = buildEffortConfigOption(modelInfos, models.currentModelId, currentEffortLevel, presentation?.useRecommendedValue === true);
9141
9310
  if (effort)
9142
9311
  options.push(effort);
@@ -34,6 +34,12 @@ export declare const AIR_SESSION_RENAME_CAPABILITY = "sessionRename";
34
34
  * {@link AIR_SESSION_INDEX_CAPABILITY} is; the client does not declare it.
35
35
  */
36
36
  export declare const AIR_SESSION_LIST_SUBSCRIBE_CAPABILITY = "sessionListSubscribe";
37
+ /**
38
+ * Session index rows carry `agentWorktree: true` when the session runs in a
39
+ * linked git worktree that Claude Code made for it. Advertised exactly when
40
+ * {@link AIR_SESSION_INDEX_CAPABILITY} is; the client does not declare it.
41
+ */
42
+ export declare const AIR_SESSION_WORKTREES_CAPABILITY = "sessionWorktrees";
37
43
  /** The `_meta.jetbrains.air` keys that the ACP tool call contract defines. */
38
44
  export declare const AIR_COMMAND_TITLE_KEY = "commandTitle";
39
45
  export declare const AIR_SUBAGENT_KEY = "subagent";
@@ -1 +1 @@
1
- {"version":3,"file":"air-extension.d.ts","sourceRoot":"","sources":["../src/air-extension.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uCAAuC,2BAA2B,CAAC;AAChF,eAAO,MAAM,0BAA0B,eAAe,CAAC;AACvD,eAAO,MAAM,8BAA8B,mBAAmB,CAAC;AAC/D,eAAO,MAAM,uCAAuC,qBAAqB,CAAC;AAC1E,eAAO,MAAM,kCAAkC,uBAAuB,CAAC;AACvE,eAAO,MAAM,yBAAyB,cAAc,CAAC;AACrD,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,sBAAsB,CAAC;AACtE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,aAAa,CAAC;AACnD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,iBAAiB,CAAC;AAC3D;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,mBAAmB,CAAC;AAC/D;;;GAGG;AACH,eAAO,MAAM,6BAA6B,kBAAkB,CAAC;AAC7D;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,yBAAyB,CAAC;AAE5E,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AACpD,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,aAAa,UAAU,CAAC;AACrC,eAAO,MAAM,0BAA0B,sBAAsB,CAAC;AAC9D,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,6FAA6F;AAC7F,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAC9C,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAC/C,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AACpD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAQ3E,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,GAAG,YAAY,EAAE,MAAM,EAAE,2BAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EAChD,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAczB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,OAAO,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAErC;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAKnF;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAY9F;AAED,4DAA4D;AAC5D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIvE"}
1
+ {"version":3,"file":"air-extension.d.ts","sourceRoot":"","sources":["../src/air-extension.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uCAAuC,2BAA2B,CAAC;AAChF,eAAO,MAAM,0BAA0B,eAAe,CAAC;AACvD,eAAO,MAAM,8BAA8B,mBAAmB,CAAC;AAC/D,eAAO,MAAM,uCAAuC,qBAAqB,CAAC;AAC1E,eAAO,MAAM,kCAAkC,uBAAuB,CAAC;AACvE,eAAO,MAAM,yBAAyB,cAAc,CAAC;AACrD,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,sBAAsB,CAAC;AACtE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,aAAa,CAAC;AACnD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,iBAAiB,CAAC;AAC3D;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,mBAAmB,CAAC;AAC/D;;;GAGG;AACH,eAAO,MAAM,6BAA6B,kBAAkB,CAAC;AAC7D;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,yBAAyB,CAAC;AAC5E;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,qBAAqB,CAAC;AAEnE,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AACpD,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,aAAa,UAAU,CAAC;AACrC,eAAO,MAAM,0BAA0B,sBAAsB,CAAC;AAC9D,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,6FAA6F;AAC7F,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAC9C,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAC/C,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AACpD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAQ3E,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,GAAG,YAAY,EAAE,MAAM,EAAE,2BAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EAChD,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAczB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,OAAO,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAErC;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAKnF;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAY9F;AAED,4DAA4D;AAC5D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIvE"}
@@ -34,6 +34,12 @@ export const AIR_SESSION_RENAME_CAPABILITY = "sessionRename";
34
34
  * {@link AIR_SESSION_INDEX_CAPABILITY} is; the client does not declare it.
35
35
  */
36
36
  export const AIR_SESSION_LIST_SUBSCRIBE_CAPABILITY = "sessionListSubscribe";
37
+ /**
38
+ * Session index rows carry `agentWorktree: true` when the session runs in a
39
+ * linked git worktree that Claude Code made for it. Advertised exactly when
40
+ * {@link AIR_SESSION_INDEX_CAPABILITY} is; the client does not declare it.
41
+ */
42
+ export const AIR_SESSION_WORKTREES_CAPABILITY = "sessionWorktrees";
37
43
  /** The `_meta.jetbrains.air` keys that the ACP tool call contract defines. */
38
44
  export const AIR_COMMAND_TITLE_KEY = "commandTitle";
39
45
  export const AIR_SUBAGENT_KEY = "subagent";