@agentclientprotocol/claude-agent-acp 0.99.2-preview.1 → 0.99.2-preview.10
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 +8 -0
- package/dist/acp-agent.d.ts +50 -7
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +315 -87
- package/dist/diff.d.ts +1 -1
- package/dist/diff.js +5 -5
- package/dist/elicitation.d.ts +1 -2
- package/dist/elicitation.d.ts.map +1 -1
- package/dist/elicitation.js +34 -16
- package/dist/fork-session.d.ts.map +1 -1
- package/dist/fork-session.js +23 -0
- package/dist/native-subagents.d.ts +49 -3
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +118 -4
- package/dist/session-index/service.d.ts +3 -1
- package/dist/session-index/service.d.ts.map +1 -1
- package/dist/session-index/service.js +5 -10
- package/dist/startup-failure.d.ts +16 -0
- package/dist/startup-failure.d.ts.map +1 -0
- package/dist/startup-failure.js +38 -0
- package/dist/subagent-history.d.ts +8 -1
- package/dist/subagent-history.d.ts.map +1 -1
- package/dist/subagent-history.js +40 -1
- package/dist/subagent-replay.d.ts +19 -3
- package/dist/subagent-replay.d.ts.map +1 -1
- package/dist/subagent-replay.js +46 -4
- package/dist/tool-calls/reporters/interaction.d.ts.map +1 -1
- package/dist/tool-calls/reporters/interaction.js +8 -3
- package/dist/v2/agent.d.ts.map +1 -1
- package/dist/v2/agent.js +40 -15
- package/dist/v2/initialize.d.ts +16 -1
- package/dist/v2/initialize.d.ts.map +1 -1
- package/dist/v2/initialize.js +26 -3
- package/dist/v2/permission.d.ts +5 -2
- package/dist/v2/permission.d.ts.map +1 -1
- package/dist/v2/permission.js +5 -2
- package/dist/v2/prompt.d.ts +2 -1
- package/dist/v2/prompt.d.ts.map +1 -1
- package/dist/v2/prompt.js +5 -1
- package/dist/v2/session.d.ts.map +1 -1
- package/dist/v2/session.js +21 -9
- package/package.json +6 -6
package/dist/acp-agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agent as acpAgent,
|
|
1
|
+
import { agent as acpAgent, methods, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk/unstable";
|
|
2
2
|
import { filterEscalatingDefaultMode, getSessionMessages, listSessions, query, resolveSettings, } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { GOAL_ACTIONS, GOAL_CONTROL_METHOD, GOAL_EXTENSION_VERSION, goalUpdateFromPrompt, parseGoalRequest, toGoalSnapshot, } from "./goal-extension.js";
|
|
4
4
|
import { blobResourceToClaude } from "./blob-resources.js";
|
|
@@ -55,6 +55,7 @@ import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableMode
|
|
|
55
55
|
import { GatewayListingError, listBedrockModels, listGatewayModels, PROVIDER_AUTH_TOKEN_PLACEHOLDER, selectBedrockRegion, } from "./gateway-models.js";
|
|
56
56
|
import { buildEffortConfigOption, EFFORT_CONFIG_ID, effortFlagSettings, effortLevelName, mergeEffortSettings, settingsEffortForModel, } from "./session-effort.js";
|
|
57
57
|
import { createUltracodeConfigOption, readUltracodeSettings, resolveUltracodeEnabled, ULTRACODE_CONFIG_ID, ultracodeSupported, ultracodeWorkflowsEnabled, } from "./session-ultracode.js";
|
|
58
|
+
import { STARTUP_FAILURE_RESULTS_ENV, startupFailureError } from "./startup-failure.js";
|
|
58
59
|
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
|
59
60
|
export { applyAvailableModelsAllowlist, matchResumedModel, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
60
61
|
const execFileAsync = promisify(execFile);
|
|
@@ -512,6 +513,16 @@ export function computeSessionFingerprint(params) {
|
|
|
512
513
|
options,
|
|
513
514
|
}));
|
|
514
515
|
}
|
|
516
|
+
/** `params` without `_meta.claudeCode.options.effort`. */
|
|
517
|
+
function withoutEffort(params) {
|
|
518
|
+
const meta = params._meta;
|
|
519
|
+
if (meta?.claudeCode?.options?.effort === undefined) {
|
|
520
|
+
return params;
|
|
521
|
+
}
|
|
522
|
+
const options = { ...meta.claudeCode.options };
|
|
523
|
+
delete options.effort;
|
|
524
|
+
return { ...params, _meta: { ...meta, claudeCode: { ...meta.claudeCode, options } } };
|
|
525
|
+
}
|
|
515
526
|
/** The JSON-RPC code ACP assigns to `authRequired`. Read from the SDK so it
|
|
516
527
|
* cannot drift from the errors this agent throws. */
|
|
517
528
|
const AUTH_REQUIRED_CODE = RequestError.authRequired().code;
|
|
@@ -1110,7 +1121,10 @@ async function authenticateMcpServer(host, sessionId, query, serverName) {
|
|
|
1110
1121
|
completed.then((authenticated) => ({ type: "completed", authenticated })),
|
|
1111
1122
|
elicitation.then((response) => ({ type: "elicitation", response })),
|
|
1112
1123
|
]);
|
|
1113
|
-
|
|
1124
|
+
// A url-mode accept carries no content to check. The guard
|
|
1125
|
+
// `CreateElicitationResponse.isAccept` would also reject an accept whose
|
|
1126
|
+
// `content` holds a v2-only value, such as an object.
|
|
1127
|
+
if (first.type === "elicitation" && first.response.action !== "accept") {
|
|
1114
1128
|
return;
|
|
1115
1129
|
}
|
|
1116
1130
|
if (first.type === "elicitation") {
|
|
@@ -1159,6 +1173,8 @@ export class ClaudeAcpAgent {
|
|
|
1159
1173
|
providerConfig;
|
|
1160
1174
|
/** Serializes provider changes while every open query is recreated between turns. */
|
|
1161
1175
|
providerUpdate = null;
|
|
1176
|
+
/** Per session: queue that runs mode/config changes and query restarts one at a time. */
|
|
1177
|
+
configurationChanges = new Map();
|
|
1162
1178
|
exitPlan;
|
|
1163
1179
|
/** Last auth identity reported to the client, connection-scoped like
|
|
1164
1180
|
* `authenticate`/`logout`. Undefined means "not determined yet". */
|
|
@@ -1207,6 +1223,12 @@ export class ClaudeAcpAgent {
|
|
|
1207
1223
|
replayedChildModels = new Map();
|
|
1208
1224
|
/** See the `foreground` constructor option. */
|
|
1209
1225
|
foregroundEvents;
|
|
1226
|
+
/** Counts the prompts `startTurn()` has received, across sessions. */
|
|
1227
|
+
promptsReceived = 0;
|
|
1228
|
+
/** Per session, the `promptsReceived` count when its last cancel started.
|
|
1229
|
+
* A prompt received before that cancel that `startTurn()` had not queued
|
|
1230
|
+
* yet ends as cancelled instead of being queued. */
|
|
1231
|
+
cancelledThrough = new Map();
|
|
1210
1232
|
/**
|
|
1211
1233
|
* The foreground work of each session while it runs, by the session id that
|
|
1212
1234
|
* the client knows, on every connection: only the `foreground` option
|
|
@@ -1490,7 +1512,7 @@ export class ClaudeAcpAgent {
|
|
|
1490
1512
|
* before the first update. A failed CLI start stops the replay.
|
|
1491
1513
|
*/
|
|
1492
1514
|
async createSessionWhileReplaying(params, timing) {
|
|
1493
|
-
await this.validateCwd(params.cwd);
|
|
1515
|
+
await this.validateCwd(params.cwd, params.additionalDirectories);
|
|
1494
1516
|
clientSystemPrompt(params.systemPrompt);
|
|
1495
1517
|
const pending = {
|
|
1496
1518
|
state: {
|
|
@@ -1548,7 +1570,8 @@ export class ClaudeAcpAgent {
|
|
|
1548
1570
|
async unstable_forkSession(params) {
|
|
1549
1571
|
while (this.providerUpdate)
|
|
1550
1572
|
await this.providerUpdate;
|
|
1551
|
-
// Reject a
|
|
1573
|
+
// Reject a bad `cwd` or prompt before copying the transcript.
|
|
1574
|
+
await this.validateCwd(params.cwd, params.additionalDirectories);
|
|
1552
1575
|
clientSystemPrompt(params.systemPrompt);
|
|
1553
1576
|
const { sessionId } = await forkSession(params, {
|
|
1554
1577
|
liveMessageIdToUuid: this.sessions[params.sessionId]?.messageIdToUuid,
|
|
@@ -1607,13 +1630,19 @@ export class ClaudeAcpAgent {
|
|
|
1607
1630
|
* reads the start and the end only of the transcripts of the page.
|
|
1608
1631
|
*/
|
|
1609
1632
|
async listSessions(params) {
|
|
1633
|
+
if (params.cwd != null && !path.isAbsolute(params.cwd)) {
|
|
1634
|
+
throw RequestError.invalidParams({ cwd: params.cwd }, `\`cwd\` must be an absolute path, but received: ${params.cwd}`);
|
|
1635
|
+
}
|
|
1610
1636
|
const indexed = this.sessionIndex.list(params);
|
|
1611
1637
|
if (indexed)
|
|
1612
1638
|
return indexed;
|
|
1613
1639
|
const offset = sessionListOffset(params.cursor);
|
|
1614
1640
|
// One more session than the page tells whether a next page exists.
|
|
1641
|
+
// `includeWorktrees: false`: a `cwd` filter matches sessions of exactly
|
|
1642
|
+
// that directory, not of the other git worktrees of its repository.
|
|
1615
1643
|
const sdkSessions = await listSessions({
|
|
1616
1644
|
dir: params.cwd ?? undefined,
|
|
1645
|
+
includeWorktrees: false,
|
|
1617
1646
|
limit: SESSION_LIST_PAGE_SIZE + 1,
|
|
1618
1647
|
offset,
|
|
1619
1648
|
});
|
|
@@ -1689,7 +1718,7 @@ export class ClaudeAcpAgent {
|
|
|
1689
1718
|
this.setAuthStatus(gatewayAuthStatus(gatewayRequestToProviderConfig(this.gatewayAuthRequest)?.baseUrl));
|
|
1690
1719
|
return;
|
|
1691
1720
|
}
|
|
1692
|
-
throw
|
|
1721
|
+
throw RequestError.invalidParams({ methodId: _params.methodId }, `Unsupported authentication method: ${_params.methodId}`);
|
|
1693
1722
|
}
|
|
1694
1723
|
/**
|
|
1695
1724
|
* Stores `next` and pushes it to the client.
|
|
@@ -2131,8 +2160,18 @@ export class ClaudeAcpAgent {
|
|
|
2131
2160
|
/**
|
|
2132
2161
|
* Serves an ACP v1 `session/prompt`, which answers when the turn ends. The
|
|
2133
2162
|
* outcome of a turn is field for field a v1 prompt response.
|
|
2163
|
+
*
|
|
2164
|
+
* A content block that the agent does not advertise, such as `audio`, fails
|
|
2165
|
+
* the request with invalid params (-32602) before anything is queued:
|
|
2166
|
+
* `promptToClaude` would drop it. v2 prompts reach `startTurn` through
|
|
2167
|
+
* `v1PromptRequest`, which rejects these blocks itself.
|
|
2134
2168
|
*/
|
|
2135
2169
|
async prompt(params) {
|
|
2170
|
+
for (const block of params.prompt) {
|
|
2171
|
+
if (!SUPPORTED_PROMPT_CONTENT_TYPES.includes(block.type)) {
|
|
2172
|
+
throw RequestError.invalidParams({ type: block.type }, `Content blocks of type ${block.type} are not supported`);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2136
2175
|
let events;
|
|
2137
2176
|
const outcome = new Promise((resolve, reject) => {
|
|
2138
2177
|
events = { inserted() { }, ended: resolve, failed: reject };
|
|
@@ -2147,6 +2186,7 @@ export class ClaudeAcpAgent {
|
|
|
2147
2186
|
* before this resolves.
|
|
2148
2187
|
*/
|
|
2149
2188
|
async startTurn(params, events) {
|
|
2189
|
+
const received = ++this.promptsReceived;
|
|
2150
2190
|
while (this.providerUpdate)
|
|
2151
2191
|
await this.providerUpdate;
|
|
2152
2192
|
let session = this.sessions[params.sessionId];
|
|
@@ -2164,8 +2204,16 @@ export class ClaudeAcpAgent {
|
|
|
2164
2204
|
this.useAuthStatusSettings(session.settingsManager, session.effortSettingsOverride, session.creationParams?._meta?.claudeCode?.options?.env);
|
|
2165
2205
|
void this.probeCliAuthStatus();
|
|
2166
2206
|
const signOutRespawn = this.respawnSignedOutSession(params.sessionId, session);
|
|
2167
|
-
if (signOutRespawn)
|
|
2168
|
-
|
|
2207
|
+
if (signOutRespawn) {
|
|
2208
|
+
await signOutRespawn;
|
|
2209
|
+
// A provider update can queue its replacement behind the sign-out respawn.
|
|
2210
|
+
// Wait for that replacement too before a prompt starts on the old route.
|
|
2211
|
+
while (this.providerUpdate)
|
|
2212
|
+
await this.providerUpdate;
|
|
2213
|
+
session = this.sessions[params.sessionId];
|
|
2214
|
+
if (!session)
|
|
2215
|
+
throw RequestError.sessionNotFound(params.sessionId);
|
|
2216
|
+
}
|
|
2169
2217
|
// The SDK query stream already terminated (see `queryClosed`); its iterator
|
|
2170
2218
|
// can't be revived, so enqueueing here would hang on a deferred that never
|
|
2171
2219
|
// settles. Fail clearly and let the client start a fresh session.
|
|
@@ -2252,7 +2300,9 @@ export class ClaudeAcpAgent {
|
|
|
2252
2300
|
// its consumer is gone, so the prompt would never be inserted. End it as
|
|
2253
2301
|
// the close ends a queued turn: `cancelled` on v1, -32800 on v2. A stream
|
|
2254
2302
|
// that ended meanwhile fails the prompt as the check above does.
|
|
2255
|
-
|
|
2303
|
+
// A `session/cancel` that arrived after this prompt, during the awaits
|
|
2304
|
+
// above, ends it the same way.
|
|
2305
|
+
if (session.tornDown || received <= (this.cancelledThrough.get(params.sessionId) ?? 0)) {
|
|
2256
2306
|
session.fileChangeReporter?.finish(fileChangeReport, "cancelled");
|
|
2257
2307
|
events.ended({ stopReason: "cancelled" });
|
|
2258
2308
|
return;
|
|
@@ -2266,6 +2316,7 @@ export class ClaudeAcpAgent {
|
|
|
2266
2316
|
// the consumer is running.
|
|
2267
2317
|
let completeTurn;
|
|
2268
2318
|
const turn = {
|
|
2319
|
+
received,
|
|
2269
2320
|
promptUuid,
|
|
2270
2321
|
events,
|
|
2271
2322
|
isLocalOnlyCommand,
|
|
@@ -2457,36 +2508,29 @@ export class ClaudeAcpAgent {
|
|
|
2457
2508
|
});
|
|
2458
2509
|
}
|
|
2459
2510
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
// Already closed by `markSessionForSignOutRespawn`; idempotent here so a
|
|
2467
|
-
// husk that reached this by another route still releases its resources.
|
|
2468
|
-
this.closeQueryStream(session);
|
|
2469
|
-
try {
|
|
2470
|
-
// The very first turn may be the one that signed out, so the CLI may
|
|
2471
|
-
// never have written the conversation.
|
|
2472
|
-
await this.recreateLiveSession(creationParams, sessionId, {
|
|
2473
|
-
permissionMode: session.modes.currentModeId,
|
|
2474
|
-
});
|
|
2475
|
-
// The new Session has the same MCP servers; keep the ones already
|
|
2476
|
-
// reported so its first `system`/init doesn't repeat their notices.
|
|
2477
|
-
const recreated = this.sessions[sessionId];
|
|
2478
|
-
if (recreated && session.reportedMcpServers) {
|
|
2479
|
-
recreated.reportedMcpServers = new Set(session.reportedMcpServers);
|
|
2511
|
+
/** Queued per session, so a choice sent during the respawn lands on the new query. */
|
|
2512
|
+
recreateSignedOutQuery(sessionId, session) {
|
|
2513
|
+
return this.serializeConfigurationChange(sessionId, async () => {
|
|
2514
|
+
const creationParams = session.creationParams;
|
|
2515
|
+
if (!creationParams) {
|
|
2516
|
+
throw RequestError.internalError(undefined, SESSION_ENDED_MESSAGE);
|
|
2480
2517
|
}
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2518
|
+
// Already replaced by a provider restart, or closed.
|
|
2519
|
+
if (this.sessions[sessionId] !== session)
|
|
2520
|
+
return;
|
|
2521
|
+
this.logger.log(`Recreating Claude session ${sessionId} after a sign-out`);
|
|
2522
|
+
try {
|
|
2523
|
+
// The signed-out turn may be the first, so the conversation may not be saved yet.
|
|
2524
|
+
await this.replaceSessionQuery(sessionId, session, creationParams);
|
|
2487
2525
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2526
|
+
catch (error) {
|
|
2527
|
+
// Keep the husk so the client can sign in and retry; a closed session stays closed.
|
|
2528
|
+
if (!session.tornDown && !this.sessions[sessionId]) {
|
|
2529
|
+
this.sessions[sessionId] = session;
|
|
2530
|
+
}
|
|
2531
|
+
throw error;
|
|
2532
|
+
}
|
|
2533
|
+
});
|
|
2490
2534
|
}
|
|
2491
2535
|
/**
|
|
2492
2536
|
* Starts a new query for the session `sessionId`, which ran on this
|
|
@@ -2510,6 +2554,7 @@ export class ClaudeAcpAgent {
|
|
|
2510
2554
|
return await this.createSession(params, {
|
|
2511
2555
|
reuseSessionId: sessionId,
|
|
2512
2556
|
permissionMode: options.permissionMode,
|
|
2557
|
+
replacingSession: options.replacingSession,
|
|
2513
2558
|
});
|
|
2514
2559
|
}
|
|
2515
2560
|
}
|
|
@@ -6442,6 +6487,12 @@ export class ClaudeAcpAgent {
|
|
|
6442
6487
|
return;
|
|
6443
6488
|
}
|
|
6444
6489
|
session.cancelled = true;
|
|
6490
|
+
// Only prompts received before this cancel are cancelled. A prompt that
|
|
6491
|
+
// arrives while the awaits below run is queued and runs after the cancel
|
|
6492
|
+
// (ACP v2: a `session/prompt` received after `session/cancel` is not
|
|
6493
|
+
// cancelled by it).
|
|
6494
|
+
const cancelledThrough = this.promptsReceived;
|
|
6495
|
+
this.cancelledThrough.set(params.sessionId, cancelledThrough);
|
|
6445
6496
|
// The interrupt below makes Claude Code withdraw the requests that are open
|
|
6446
6497
|
// now (see `withPendingUserInput`).
|
|
6447
6498
|
for (const input of session.openUserInputs ?? [])
|
|
@@ -6450,7 +6501,7 @@ export class ClaudeAcpAgent {
|
|
|
6450
6501
|
// the meantime is already judged against these prompts.
|
|
6451
6502
|
session.cancelledPrompts ??= new Set();
|
|
6452
6503
|
for (const turn of session.turnQueue ?? []) {
|
|
6453
|
-
if (turn.settled)
|
|
6504
|
+
if (turn.settled || turn.received > cancelledThrough)
|
|
6454
6505
|
continue;
|
|
6455
6506
|
session.cancelledPrompts.add(turn.promptUuid);
|
|
6456
6507
|
for (const uuid of turn.steeredUuids ?? [])
|
|
@@ -6525,7 +6576,7 @@ export class ClaudeAcpAgent {
|
|
|
6525
6576
|
const orphanedTurns = [];
|
|
6526
6577
|
if (session.turnQueue) {
|
|
6527
6578
|
for (const turn of session.turnQueue) {
|
|
6528
|
-
if (turn !== session.activeTurn && !turn.settled) {
|
|
6579
|
+
if (turn !== session.activeTurn && !turn.settled && turn.received <= cancelledThrough) {
|
|
6529
6580
|
session.fileChangeReporter?.finish(turn.fileChangeReport, "cancelled");
|
|
6530
6581
|
turn.settled = true;
|
|
6531
6582
|
// Deliberately no `usage`: a queued turn never ran, so the session
|
|
@@ -6579,7 +6630,7 @@ export class ClaudeAcpAgent {
|
|
|
6579
6630
|
}
|
|
6580
6631
|
this.trackOrphanCommand(session, turn.promptUuid, turn.commandStarted ? "started" : "pending");
|
|
6581
6632
|
}
|
|
6582
|
-
session.turnQueue = session.turnQueue.filter((turn) =>
|
|
6633
|
+
session.turnQueue = session.turnQueue.filter((turn) => !turn.settled);
|
|
6583
6634
|
}
|
|
6584
6635
|
const diagnosticTurns = orphanedTurns.filter((turn) => {
|
|
6585
6636
|
if (turn.commandFinished === "completed" ||
|
|
@@ -6702,8 +6753,8 @@ export class ClaudeAcpAgent {
|
|
|
6702
6753
|
}
|
|
6703
6754
|
// Before the interrupt: once it stops the running turn, Claude Code starts
|
|
6704
6755
|
// the next queued message right away.
|
|
6705
|
-
// A
|
|
6706
|
-
//
|
|
6756
|
+
// A steer that arrived while this cancel awaited is abandoned too, after
|
|
6757
|
+
// the record above was taken.
|
|
6707
6758
|
for (const uuid of abandoned)
|
|
6708
6759
|
(session.cancelledPrompts ??= new Set()).add(uuid);
|
|
6709
6760
|
this.dropQueuedMessages(params.sessionId, session.query, abandoned);
|
|
@@ -6853,6 +6904,7 @@ export class ClaudeAcpAgent {
|
|
|
6853
6904
|
session.nativeSubagentRuntime?.clear();
|
|
6854
6905
|
session.asyncTaskRuntime?.clear();
|
|
6855
6906
|
this.replayedChildModels.delete(sessionId);
|
|
6907
|
+
this.cancelledThrough.delete(sessionId);
|
|
6856
6908
|
delete this.sessions[sessionId];
|
|
6857
6909
|
this.sessionIndex.onTeardown(sessionId);
|
|
6858
6910
|
const ended = await raceTimeoutAndAbort(turnsEnded, TEARDOWN_TURN_END_TIMEOUT_MS, new AbortController().signal);
|
|
@@ -6895,10 +6947,13 @@ export class ClaudeAcpAgent {
|
|
|
6895
6947
|
async deleteSession(params) {
|
|
6896
6948
|
return this.sessionIndex.deleteSession(params);
|
|
6897
6949
|
}
|
|
6898
|
-
|
|
6899
|
-
return this.sessionModes.setSessionMode(params);
|
|
6950
|
+
setSessionMode(params) {
|
|
6951
|
+
return this.serializeConfigurationChange(params.sessionId, () => this.sessionModes.setSessionMode(params));
|
|
6952
|
+
}
|
|
6953
|
+
setSessionConfigOption(params) {
|
|
6954
|
+
return this.serializeConfigurationChange(params.sessionId, () => this.selectSessionConfigOption(params));
|
|
6900
6955
|
}
|
|
6901
|
-
async
|
|
6956
|
+
async selectSessionConfigOption(params) {
|
|
6902
6957
|
const session = this.sessions[params.sessionId];
|
|
6903
6958
|
if (!session) {
|
|
6904
6959
|
throw RequestError.sessionNotFound(params.sessionId);
|
|
@@ -6914,6 +6969,13 @@ export class ClaudeAcpAgent {
|
|
|
6914
6969
|
if (!option) {
|
|
6915
6970
|
throw RequestError.invalidParams(undefined, `Unknown config option: ${params.configId}`);
|
|
6916
6971
|
}
|
|
6972
|
+
// The value must match the type the option was advertised with: a
|
|
6973
|
+
// `type: "boolean"` option takes `true`/`false`, not "on"/"off", and a
|
|
6974
|
+
// `type: "select"` option takes a string.
|
|
6975
|
+
const expectedType = option.type === "boolean" ? "boolean" : "string";
|
|
6976
|
+
if (typeof params.value !== expectedType) {
|
|
6977
|
+
throw RequestError.invalidParams({ configId: params.configId, value: params.value }, `Config option ${params.configId} takes a ${expectedType} value, but received: ${JSON.stringify(params.value)}`);
|
|
6978
|
+
}
|
|
6917
6979
|
// Fast mode and ultracode carry a boolean value (for Clients that opted
|
|
6918
6980
|
// into boolean config options) or the "on"/"off" select fallback, so they
|
|
6919
6981
|
// bypass the
|
|
@@ -7131,7 +7193,7 @@ export class ClaudeAcpAgent {
|
|
|
7131
7193
|
? await subagentHistory(sessionId)
|
|
7132
7194
|
: { agents: new Map() };
|
|
7133
7195
|
const rfdReplay = form === "rfd"
|
|
7134
|
-
? new RfdSubagentReplay(rfdHistory, (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), this.subagentModels(sessionId), !pending)
|
|
7196
|
+
? new RfdSubagentReplay(rfdHistory, (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), this.subagentModels(sessionId), !pending, session?.cwd)
|
|
7135
7197
|
: undefined;
|
|
7136
7198
|
rfdReplay?.scan(messages, sessionId);
|
|
7137
7199
|
// History persists no task_started frame, so a background task gets its
|
|
@@ -7679,9 +7741,11 @@ export class ClaudeAcpAgent {
|
|
|
7679
7741
|
* `signal` through as a `cancellationSignal`. When the turn is cancelled
|
|
7680
7742
|
* while the client's prompt is still open the signal aborts, the SDK sends
|
|
7681
7743
|
* `$/cancel_request`, and our local abort race settles even if the client
|
|
7682
|
-
* ignores it. A
|
|
7683
|
-
*
|
|
7684
|
-
|
|
7744
|
+
* ignores it. A request rejection and a local abort surface the same
|
|
7745
|
+
* "Tool use aborted" that the callers expect. `canUseTool` decides what a
|
|
7746
|
+
* `cancelled` outcome means. */
|
|
7747
|
+
async requestPermissionFromClient(params, toolName, toolSignal, parentToolUseId, ownerSessionId, agentID, toolInput, previewContent) {
|
|
7748
|
+
const signal = this.untilSessionClosed(ownerSessionId, toolSignal);
|
|
7685
7749
|
if (signal.aborted)
|
|
7686
7750
|
throw new Error("Tool use aborted");
|
|
7687
7751
|
// The SDK may invoke `canUseTool` (and therefore this permission request)
|
|
@@ -7719,6 +7783,15 @@ export class ClaudeAcpAgent {
|
|
|
7719
7783
|
elicitFromClient(request, signal) {
|
|
7720
7784
|
return raceWithAbort(this.client.createElicitation(request, signal), signal);
|
|
7721
7785
|
}
|
|
7786
|
+
/** `signal`, also aborted when `teardownSession` aborts the
|
|
7787
|
+
* `abortController` of session `sessionId`. Claude Code aborts a callback's
|
|
7788
|
+
* own signal when its query closes, but not necessarily before
|
|
7789
|
+
* `session/close` responds, and v2 requires every pending elicitation of
|
|
7790
|
+
* the session to be cancelled with `$/cancel_request` before that response. */
|
|
7791
|
+
untilSessionClosed(sessionId, signal) {
|
|
7792
|
+
const sessionSignal = this.sessions[sessionId]?.abortController?.signal;
|
|
7793
|
+
return sessionSignal ? AbortSignal.any([signal, sessionSignal]) : signal;
|
|
7794
|
+
}
|
|
7722
7795
|
/** Settle the tool call `ensureToolCallEmitted` emitted for a
|
|
7723
7796
|
* SandboxNetworkAccess request once the request is answered. The callback
|
|
7724
7797
|
* asks for permission and runs nothing, so the SDK never sends a
|
|
@@ -7852,6 +7925,11 @@ export class ClaudeAcpAgent {
|
|
|
7852
7925
|
const session = this.sessions[sessionId];
|
|
7853
7926
|
if (!session || !input.agent_id)
|
|
7854
7927
|
return { continue: true };
|
|
7928
|
+
// A hook's `cwd` is the child's working directory, e.g. its worktree.
|
|
7929
|
+
// Reported before the hook returns, so before the work that follows it.
|
|
7930
|
+
await session.nativeSubagentRuntime
|
|
7931
|
+
?.reportCwd(input.agent_id, input.cwd)
|
|
7932
|
+
.catch((error) => this.logger.error(`Session ${sessionId}: failed to report subagent cwd: ${error}`));
|
|
7855
7933
|
const options = [];
|
|
7856
7934
|
if (input.permission_mode) {
|
|
7857
7935
|
const mode = session.modes.availableModes.find((mode) => mode.id === input.permission_mode);
|
|
@@ -8077,9 +8155,13 @@ export class ClaudeAcpAgent {
|
|
|
8077
8155
|
// cancel does not stop. Answer such a request with a denial so the
|
|
8078
8156
|
// subagent reads "the user refused" and goes on. Throwing instead makes
|
|
8079
8157
|
// Claude Code deny it as "Tool permission request failed".
|
|
8158
|
+
// On ACP v2, `cancelled` also answers a prompt that the user dismissed
|
|
8159
|
+
// or that the client declined by policy. Without a `session/cancel`
|
|
8160
|
+
// (`session.cancelled` unset), that is the user refusing the tool, so it
|
|
8161
|
+
// gets the same denial and the turn goes on.
|
|
8080
8162
|
if (response.outcome?.outcome === "cancelled" &&
|
|
8081
8163
|
this.sessions[sessionId] === session &&
|
|
8082
|
-
!blocksForeground(session, agentID)) {
|
|
8164
|
+
(!blocksForeground(session, agentID) || (this.v2 && !session.cancelled))) {
|
|
8083
8165
|
return {
|
|
8084
8166
|
behavior: "deny",
|
|
8085
8167
|
message: "User refused permission to run tool",
|
|
@@ -8143,7 +8225,8 @@ export class ClaudeAcpAgent {
|
|
|
8143
8225
|
* an error, is a `cancel`.
|
|
8144
8226
|
*/
|
|
8145
8227
|
handleMcpElicitation(sessionId, support) {
|
|
8146
|
-
return async (request, { signal }) => {
|
|
8228
|
+
return async (request, { signal: callbackSignal }) => {
|
|
8229
|
+
const signal = this.untilSessionClosed(sessionId, callbackSignal);
|
|
8147
8230
|
const isUrl = request.mode === "url";
|
|
8148
8231
|
if ((isUrl && !support.url) || (!isUrl && !support.form)) {
|
|
8149
8232
|
return { action: "cancel" };
|
|
@@ -8180,7 +8263,8 @@ export class ClaudeAcpAgent {
|
|
|
8180
8263
|
* The elicitation goes to `requestSessionId`, a subagent's child session for
|
|
8181
8264
|
* a question it asks, and blocks the turn of `sessionId`.
|
|
8182
8265
|
*/
|
|
8183
|
-
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID,
|
|
8266
|
+
async handleAskUserQuestion(sessionId, requestSessionId, toolInput, toolUseID, toolSignal, agentID) {
|
|
8267
|
+
const signal = this.untilSessionClosed(sessionId, toolSignal);
|
|
8184
8268
|
const questions = extractAskUserQuestions(toolInput);
|
|
8185
8269
|
if (!questions) {
|
|
8186
8270
|
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
@@ -8219,7 +8303,8 @@ export class ClaudeAcpAgent {
|
|
|
8219
8303
|
* refusal-fallback consent prompt, rendered as an ACP form elicitation.
|
|
8220
8304
|
*/
|
|
8221
8305
|
handleUserDialog(sessionId) {
|
|
8222
|
-
return async (request, { signal }) => {
|
|
8306
|
+
return async (request, { signal: callbackSignal }) => {
|
|
8307
|
+
const signal = this.untilSessionClosed(sessionId, callbackSignal);
|
|
8223
8308
|
if (request.dialogKind !== REFUSAL_FALLBACK_DIALOG_KIND) {
|
|
8224
8309
|
return { behavior: "cancelled" };
|
|
8225
8310
|
}
|
|
@@ -8323,19 +8408,27 @@ export class ClaudeAcpAgent {
|
|
|
8323
8408
|
/**
|
|
8324
8409
|
* The work that follows a session setup response: the session's commands,
|
|
8325
8410
|
* the current states of the subagents a load replayed, and MCP server
|
|
8326
|
-
* authentication.
|
|
8327
|
-
*
|
|
8328
|
-
* response (`src/v2/agent.ts`), so it gets neither here.
|
|
8411
|
+
* authentication. On v2 the setup handlers of `src/v2/agent.ts` do this
|
|
8412
|
+
* work once they have sent their response, so it does nothing here.
|
|
8329
8413
|
*/
|
|
8330
8414
|
afterSetupResponse(sessionId, mcpServers) {
|
|
8415
|
+
if (this.v2)
|
|
8416
|
+
return;
|
|
8331
8417
|
setTimeout(() => {
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
void this.publishCurrentSubagentStates(sessionId);
|
|
8335
|
-
}
|
|
8418
|
+
this.sendAvailableCommandsUpdate(sessionId);
|
|
8419
|
+
void this.publishCurrentSubagentStates(sessionId);
|
|
8336
8420
|
startMcpAuthentication(this, sessionId, mcpServers);
|
|
8337
8421
|
}, 0);
|
|
8338
8422
|
}
|
|
8423
|
+
/**
|
|
8424
|
+
* Starts OAuth in the background for the servers of `mcpServers` that
|
|
8425
|
+
* Claude Code reports as needing it, which sends the client a URL
|
|
8426
|
+
* elicitation. The v2 surface calls it after the setup response, since v2
|
|
8427
|
+
* asks for no elicitation before the `session/new` response.
|
|
8428
|
+
*/
|
|
8429
|
+
authenticateMcpServers(sessionId, mcpServers) {
|
|
8430
|
+
startMcpAuthentication(this, sessionId, mcpServers);
|
|
8431
|
+
}
|
|
8339
8432
|
/**
|
|
8340
8433
|
* Sends the current state of each subagent that the last load of
|
|
8341
8434
|
* `sessionId` replayed (see {@link RfdSubagentReplay.currentStates}). Run it
|
|
@@ -8595,6 +8688,7 @@ export class ClaudeAcpAgent {
|
|
|
8595
8688
|
agent: value === DEFAULT_AGENT_ID ? null : value,
|
|
8596
8689
|
});
|
|
8597
8690
|
session.currentAgent = value;
|
|
8691
|
+
session.agentSelected = true;
|
|
8598
8692
|
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
8599
8693
|
}
|
|
8600
8694
|
else if (configId === EFFORT_CONFIG_ID) {
|
|
@@ -8740,7 +8834,8 @@ export class ClaudeAcpAgent {
|
|
|
8740
8834
|
});
|
|
8741
8835
|
}
|
|
8742
8836
|
async getOrCreateSession(params, resumedSession) {
|
|
8743
|
-
// Reject a
|
|
8837
|
+
// Reject a bad `cwd` or prompt before reusing or tearing down a live session.
|
|
8838
|
+
await this.validateCwd(params.cwd, params.additionalDirectories);
|
|
8744
8839
|
clientSystemPrompt(params.systemPrompt);
|
|
8745
8840
|
const existingSession = this.sessions[params.sessionId];
|
|
8746
8841
|
// A recreated live session keeps its mode, not the mode of the transcript.
|
|
@@ -8788,8 +8883,16 @@ export class ClaudeAcpAgent {
|
|
|
8788
8883
|
* directory before we create a session. Throws an `invalidParams` error with
|
|
8789
8884
|
* an actionable message so clients (e.g. Zed) can surface it to the user
|
|
8790
8885
|
* instead of failing later with an opaque SDK error.
|
|
8886
|
+
*
|
|
8887
|
+
* Each `additionalDirectories` entry must be absolute too. Only the lexical
|
|
8888
|
+
* rule is checked there: a missing extra root does not fail a session.
|
|
8791
8889
|
*/
|
|
8792
|
-
async validateCwd(cwd) {
|
|
8890
|
+
async validateCwd(cwd, additionalDirectories) {
|
|
8891
|
+
for (const dir of additionalDirectories ?? []) {
|
|
8892
|
+
if (!path.isAbsolute(dir)) {
|
|
8893
|
+
throw RequestError.invalidParams({ additionalDirectories }, `\`additionalDirectories\` entries must be absolute paths, but received: ${dir}`);
|
|
8894
|
+
}
|
|
8895
|
+
}
|
|
8793
8896
|
if (!path.isAbsolute(cwd)) {
|
|
8794
8897
|
throw RequestError.invalidParams({ cwd }, `\`cwd\` must be an absolute path, but received: ${cwd}`);
|
|
8795
8898
|
}
|
|
@@ -8817,7 +8920,7 @@ export class ClaudeAcpAgent {
|
|
|
8817
8920
|
// this check a session is created against a missing directory and the
|
|
8818
8921
|
// failure only surfaces later as a confusing "native binary failed to
|
|
8819
8922
|
// launch" error from the SDK (see issue #749).
|
|
8820
|
-
await this.validateCwd(params.cwd);
|
|
8923
|
+
await this.validateCwd(params.cwd, params.additionalDirectories);
|
|
8821
8924
|
const clientPrompt = clientSystemPrompt(params.systemPrompt);
|
|
8822
8925
|
// We want to create a new session id unless it is resume,
|
|
8823
8926
|
// but not resume + forkSession.
|
|
@@ -9016,6 +9119,7 @@ export class ClaudeAcpAgent {
|
|
|
9016
9119
|
...providerEnv,
|
|
9017
9120
|
// Opt-in to session state events like when the agent is idle
|
|
9018
9121
|
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
9122
|
+
...STARTUP_FAILURE_RESULTS_ENV,
|
|
9019
9123
|
};
|
|
9020
9124
|
// Scopes the context-window cache to this query's backend (see
|
|
9021
9125
|
// `contextWindowCache`). Derived from the same `env` object handed to the
|
|
@@ -9260,7 +9364,7 @@ export class ClaudeAcpAgent {
|
|
|
9260
9364
|
error.message.includes("No conversation found with session ID"))) {
|
|
9261
9365
|
throw RequestError.sessionNotFound(sessionId);
|
|
9262
9366
|
}
|
|
9263
|
-
throw error;
|
|
9367
|
+
throw startupFailureError(error) ?? error;
|
|
9264
9368
|
}
|
|
9265
9369
|
timing.phase("sdk-initialize");
|
|
9266
9370
|
// Publish the identity BEFORE the guard can refuse this session. A
|
|
@@ -9427,6 +9531,9 @@ export class ClaudeAcpAgent {
|
|
|
9427
9531
|
// "opus[1m]" row matched for a bare 200k id), so on the fallback path only
|
|
9428
9532
|
// the id itself is a trustworthy window signal.
|
|
9429
9533
|
const seededWindow = immediateContextWindow(providerCacheKey, models.currentModelId, allowlistedModelInfo);
|
|
9534
|
+
if (creationOpts.replacingSession?.tornDown) {
|
|
9535
|
+
throw RequestError.internalError(undefined, SESSION_ENDED_MESSAGE);
|
|
9536
|
+
}
|
|
9430
9537
|
this.sessions[sessionId] = {
|
|
9431
9538
|
query: q,
|
|
9432
9539
|
input: input,
|
|
@@ -9537,27 +9644,8 @@ export class ClaudeAcpAgent {
|
|
|
9537
9644
|
await Promise.all(activeTurns);
|
|
9538
9645
|
}
|
|
9539
9646
|
this.providerConfig = config;
|
|
9540
|
-
for (const [sessionId
|
|
9541
|
-
|
|
9542
|
-
continue;
|
|
9543
|
-
}
|
|
9544
|
-
this.logger.log(`Recreating Claude session ${sessionId} for provider update`);
|
|
9545
|
-
this.closeQueryStream(session);
|
|
9546
|
-
delete this.sessions[sessionId];
|
|
9547
|
-
try {
|
|
9548
|
-
await this.createSession(session.creationParams, {
|
|
9549
|
-
resume: sessionId,
|
|
9550
|
-
permissionMode: session.modes.currentModeId,
|
|
9551
|
-
});
|
|
9552
|
-
}
|
|
9553
|
-
catch (error) {
|
|
9554
|
-
// One session that cannot come back must not abort the switch. The
|
|
9555
|
-
// `--hide-claude-auth` guard makes this a normal outcome of
|
|
9556
|
-
// `providers/disable`: the override kept a subscription account
|
|
9557
|
-
// usable, and creation refuses without it. The session is already
|
|
9558
|
-
// gone, so tell the client why and go on to the next one.
|
|
9559
|
-
this.reportSessionLostOnProviderUpdate(sessionId, session, error);
|
|
9560
|
-
}
|
|
9647
|
+
for (const [sessionId] of sessions) {
|
|
9648
|
+
await this.serializeConfigurationChange(sessionId, () => this.restartSessionForProvider(sessionId));
|
|
9561
9649
|
}
|
|
9562
9650
|
});
|
|
9563
9651
|
// Sessions and prompts await `providerUpdate` before they run. A rejected
|
|
@@ -9577,6 +9665,126 @@ export class ClaudeAcpAgent {
|
|
|
9577
9665
|
}
|
|
9578
9666
|
}
|
|
9579
9667
|
}
|
|
9668
|
+
/** Starts a new query for the session under the new provider and restores its choices. */
|
|
9669
|
+
async restartSessionForProvider(sessionId) {
|
|
9670
|
+
// A sign-out respawn ahead of this change can have replaced the snapshot.
|
|
9671
|
+
const session = this.sessions[sessionId];
|
|
9672
|
+
if (!session?.creationParams) {
|
|
9673
|
+
return;
|
|
9674
|
+
}
|
|
9675
|
+
this.logger.log(`Recreating Claude session ${sessionId} for provider update`);
|
|
9676
|
+
try {
|
|
9677
|
+
await this.replaceSessionQuery(sessionId, session, session.creationParams);
|
|
9678
|
+
}
|
|
9679
|
+
catch (error) {
|
|
9680
|
+
if (this.sessions[sessionId] === session)
|
|
9681
|
+
delete this.sessions[sessionId];
|
|
9682
|
+
if (session.tornDown)
|
|
9683
|
+
return;
|
|
9684
|
+
// One session that cannot come back must not abort the switch. The
|
|
9685
|
+
// `--hide-claude-auth` guard makes this a normal outcome of
|
|
9686
|
+
// `providers/disable`: the override kept a subscription account
|
|
9687
|
+
// usable, and creation refuses without it. The session is already
|
|
9688
|
+
// gone, so tell the client why and go on to the next one.
|
|
9689
|
+
this.reportSessionLostOnProviderUpdate(sessionId, session, error);
|
|
9690
|
+
}
|
|
9691
|
+
}
|
|
9692
|
+
/** Recreates the session's query and restores its choices and reported MCP servers, unless it was closed meanwhile. */
|
|
9693
|
+
async replaceSessionQuery(sessionId, session, creationParams) {
|
|
9694
|
+
this.closeQueryStream(session);
|
|
9695
|
+
// Keep the old record visible so closeSession and dispose can mark it torn down.
|
|
9696
|
+
// Without the creation-time effort: restoreSessionChoices restores the current pin instead.
|
|
9697
|
+
await this.recreateLiveSession(withoutEffort(creationParams), sessionId, {
|
|
9698
|
+
permissionMode: session.modes.currentModeId,
|
|
9699
|
+
replacingSession: session,
|
|
9700
|
+
});
|
|
9701
|
+
const recreated = this.sessions[sessionId];
|
|
9702
|
+
if (!recreated || recreated.tornDown)
|
|
9703
|
+
return;
|
|
9704
|
+
// Same MCP servers, so don't repeat their notices.
|
|
9705
|
+
if (session.reportedMcpServers) {
|
|
9706
|
+
recreated.reportedMcpServers = new Set(session.reportedMcpServers);
|
|
9707
|
+
}
|
|
9708
|
+
await this.restoreSessionChoices(sessionId, recreated, session);
|
|
9709
|
+
if (recreated.tornDown)
|
|
9710
|
+
return;
|
|
9711
|
+
// The recreated session works without this update, so a failed send is only logged.
|
|
9712
|
+
await this.client
|
|
9713
|
+
.sessionUpdate({
|
|
9714
|
+
sessionId,
|
|
9715
|
+
update: { sessionUpdate: "config_option_update", configOptions: recreated.configOptions },
|
|
9716
|
+
})
|
|
9717
|
+
.catch((error) => {
|
|
9718
|
+
this.logger.error(`Session ${sessionId}: could not publish the configuration restored after the query restart:`, error);
|
|
9719
|
+
});
|
|
9720
|
+
}
|
|
9721
|
+
/** Re-applies the choices of `previous` to the session that replaced it; a choice the new query rejects is logged. */
|
|
9722
|
+
async restoreSessionChoices(sessionId, session, previous) {
|
|
9723
|
+
const restore = async (choice, apply) => {
|
|
9724
|
+
if (session.tornDown)
|
|
9725
|
+
return;
|
|
9726
|
+
try {
|
|
9727
|
+
await apply();
|
|
9728
|
+
}
|
|
9729
|
+
catch (error) {
|
|
9730
|
+
if (!session.tornDown) {
|
|
9731
|
+
this.logger.error(`Could not restore ${choice} after the query restart:`, error);
|
|
9732
|
+
}
|
|
9733
|
+
}
|
|
9734
|
+
};
|
|
9735
|
+
const select = (configId, value) => restore(`${configId} "${value}"`, () => this.selectSessionConfigOption({ sessionId, configId, value }));
|
|
9736
|
+
// First: the model decides which modes, effort levels, Fast mode and ultracode exist.
|
|
9737
|
+
if (session.models.currentModelId !== previous.models.currentModelId) {
|
|
9738
|
+
await select(MODEL_CONFIG_ID, previous.models.currentModelId);
|
|
9739
|
+
}
|
|
9740
|
+
// After the model: creation falls back from "auto" to acceptEdits on a model without Auto.
|
|
9741
|
+
if (session.modes.currentModeId !== previous.modes.currentModeId) {
|
|
9742
|
+
await select(MODE_CONFIG_ID, previous.modes.currentModeId);
|
|
9743
|
+
if (session.modes.currentModeId === previous.modes.currentModeId) {
|
|
9744
|
+
session.autoModeFallbackWarningPending = false;
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9747
|
+
if (session.modes.currentModeId === "plan") {
|
|
9748
|
+
session.prePlanMode = previous.prePlanMode;
|
|
9749
|
+
}
|
|
9750
|
+
// Compare intent, not the option: the option is hidden on a model without support.
|
|
9751
|
+
if (session.ultracodeEnabled !== previous.ultracodeEnabled) {
|
|
9752
|
+
await restore("ultracode", () => this.applyUltracode(session, previous.ultracodeEnabled));
|
|
9753
|
+
}
|
|
9754
|
+
if (session.fastModeEnabled !== previous.fastModeEnabled) {
|
|
9755
|
+
await restore("Fast mode", () => this.applyFastMode(session, previous.fastModeEnabled));
|
|
9756
|
+
}
|
|
9757
|
+
// After ultracode, since the effort flag carries it.
|
|
9758
|
+
if (previous.effortPinnedLevel !== undefined) {
|
|
9759
|
+
await select(EFFORT_CONFIG_ID, previous.effortPinnedLevel);
|
|
9760
|
+
}
|
|
9761
|
+
// Even when unchanged: Default over a hidden creation-time agent like "Explore" must clear it.
|
|
9762
|
+
if (previous.agentSelected) {
|
|
9763
|
+
if (previous.currentAgent === DEFAULT_AGENT_ID) {
|
|
9764
|
+
// Default is always valid, even when discovery returned no custom agents and hid the picker.
|
|
9765
|
+
await restore("Default agent", () => this.applyConfigOptionValue(sessionId, session, AGENT_CONFIG_ID, DEFAULT_AGENT_ID));
|
|
9766
|
+
}
|
|
9767
|
+
else {
|
|
9768
|
+
await select(AGENT_CONFIG_ID, previous.currentAgent);
|
|
9769
|
+
}
|
|
9770
|
+
}
|
|
9771
|
+
// Keep the original params and fingerprint paired for later load/resume calls.
|
|
9772
|
+
session.creationParams = previous.creationParams;
|
|
9773
|
+
session.sessionFingerprint = previous.sessionFingerprint;
|
|
9774
|
+
}
|
|
9775
|
+
serializeConfigurationChange(sessionId, change) {
|
|
9776
|
+
const result = (this.configurationChanges.get(sessionId) ?? Promise.resolve()).then(change);
|
|
9777
|
+
// Drops the entry once no newer change has queued behind this one.
|
|
9778
|
+
const tail = result
|
|
9779
|
+
.catch(() => { })
|
|
9780
|
+
.then(() => {
|
|
9781
|
+
if (this.configurationChanges.get(sessionId) === tail) {
|
|
9782
|
+
this.configurationChanges.delete(sessionId);
|
|
9783
|
+
}
|
|
9784
|
+
});
|
|
9785
|
+
this.configurationChanges.set(sessionId, tail);
|
|
9786
|
+
return result;
|
|
9787
|
+
}
|
|
9580
9788
|
/** Tell a capable client when a non-auth error ends a session during a provider switch. */
|
|
9581
9789
|
reportSessionLostOnProviderUpdate(sessionId, session, error) {
|
|
9582
9790
|
this.logger.error(`Session ${sessionId}: could not be recreated for the provider update: ${error}`);
|
|
@@ -10289,11 +10497,20 @@ skillFiles) {
|
|
|
10289
10497
|
};
|
|
10290
10498
|
})
|
|
10291
10499
|
.filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
|
|
10500
|
+
// Command names must not contain whitespace and must be unique. A name
|
|
10501
|
+
// like "my command" is dropped, and of two commands named "review" only the
|
|
10502
|
+
// first is kept.
|
|
10503
|
+
const names = new Set();
|
|
10292
10504
|
return [
|
|
10293
10505
|
...advertised,
|
|
10294
10506
|
MCP_AVAILABLE_COMMAND,
|
|
10295
10507
|
...remoteControlCommands.filter((command) => !claudeCodeOffersCommand(commands, terminalCommands, command.name)),
|
|
10296
|
-
]
|
|
10508
|
+
].filter((command) => {
|
|
10509
|
+
if (/\s/.test(command.name) || names.has(command.name))
|
|
10510
|
+
return false;
|
|
10511
|
+
names.add(command.name);
|
|
10512
|
+
return true;
|
|
10513
|
+
});
|
|
10297
10514
|
}
|
|
10298
10515
|
function formatUriAsLink(uri) {
|
|
10299
10516
|
try {
|
|
@@ -10313,15 +10530,26 @@ function formatUriAsLink(uri) {
|
|
|
10313
10530
|
return uri;
|
|
10314
10531
|
}
|
|
10315
10532
|
}
|
|
10533
|
+
/** The prompt content block types that `promptToClaude` converts. The agent
|
|
10534
|
+
* advertises no `audio` prompt capability. */
|
|
10535
|
+
const SUPPORTED_PROMPT_CONTENT_TYPES = [
|
|
10536
|
+
"text",
|
|
10537
|
+
"image",
|
|
10538
|
+
"resource_link",
|
|
10539
|
+
"resource",
|
|
10540
|
+
];
|
|
10316
10541
|
export function promptToClaude(prompt) {
|
|
10317
10542
|
const content = [];
|
|
10318
10543
|
const context = [];
|
|
10544
|
+
const firstText = prompt.prompt.find((chunk) => chunk.type === "text");
|
|
10319
10545
|
for (const chunk of prompt.prompt) {
|
|
10320
10546
|
switch (chunk.type) {
|
|
10321
10547
|
case "text": {
|
|
10322
10548
|
let text = chunk.text;
|
|
10323
|
-
//
|
|
10324
|
-
|
|
10549
|
+
// A slash command is the first text block. Change
|
|
10550
|
+
// `/mcp:server:command args` to `/server:command (MCP) args`; the
|
|
10551
|
+
// arguments can span lines.
|
|
10552
|
+
const mcpMatch = chunk === firstText ? text.match(/^\/mcp:([^:\s]+):(\S+)(?:\s+([\s\S]*))?$/) : null;
|
|
10325
10553
|
if (mcpMatch) {
|
|
10326
10554
|
const [, server, command, args] = mcpMatch;
|
|
10327
10555
|
text = `/${server}:${command} (MCP)${args ? ` ${args}` : ""}`;
|