@lucascouts/claude-agent-acp-plus 0.32.0 → 0.34.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.
- package/README.md +33 -3
- package/dist/accounts.d.ts +1 -1
- package/dist/accounts.d.ts.map +1 -1
- package/dist/acp-agent.d.ts +41 -2
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +387 -23
- package/dist/acp-subagents.d.ts +20 -0
- package/dist/acp-subagents.d.ts.map +1 -0
- package/dist/acp-subagents.js +14 -0
- package/dist/child-session-history.d.ts +26 -0
- package/dist/child-session-history.d.ts.map +1 -0
- package/dist/child-session-history.js +42 -0
- package/dist/child-sessions.d.ts +95 -0
- package/dist/child-sessions.d.ts.map +1 -0
- package/dist/child-sessions.js +247 -0
- package/dist/classifier-escalation.d.ts +1 -1
- package/dist/classifier-escalation.d.ts.map +1 -1
- package/dist/clear-context-coordinator.d.ts +1 -1
- package/dist/clear-context-coordinator.d.ts.map +1 -1
- package/dist/context-compaction.d.ts +1 -1
- package/dist/context-compaction.d.ts.map +1 -1
- package/dist/elicitation.d.ts +2 -2
- package/dist/elicitation.d.ts.map +1 -1
- package/dist/elicitation.js +1 -1
- package/dist/exit-plan.d.ts +1 -1
- package/dist/exit-plan.d.ts.map +1 -1
- package/dist/goal-extension.js +1 -1
- package/dist/permissions/effects.d.ts +1 -1
- package/dist/permissions/effects.d.ts.map +1 -1
- package/dist/permissions/options/filesystem.d.ts +1 -1
- package/dist/permissions/options/filesystem.d.ts.map +1 -1
- package/dist/permissions/options/shared.d.ts +1 -1
- package/dist/permissions/options/shared.d.ts.map +1 -1
- package/dist/permissions/options/shell.d.ts +1 -1
- package/dist/permissions/options/shell.d.ts.map +1 -1
- package/dist/permissions/options/tools.d.ts +1 -1
- package/dist/permissions/options/tools.d.ts.map +1 -1
- package/dist/permissions/options.d.ts +1 -1
- package/dist/permissions/options.d.ts.map +1 -1
- package/dist/permissions/presentation.d.ts +1 -1
- package/dist/permissions/presentation.d.ts.map +1 -1
- package/dist/permissions/response.d.ts +2 -2
- package/dist/permissions/response.d.ts.map +1 -1
- package/dist/plan-updates.d.ts +1 -1
- package/dist/plan-updates.d.ts.map +1 -1
- package/dist/session-failure-extension.d.ts +1 -1
- package/dist/session-failure-extension.d.ts.map +1 -1
- package/dist/session-mode.d.ts +1 -1
- package/dist/session-mode.d.ts.map +1 -1
- package/dist/session-mode.js +1 -1
- package/dist/session-notices.d.ts +1 -1
- package/dist/session-notices.d.ts.map +1 -1
- package/dist/session-titles.d.ts +1 -1
- package/dist/session-titles.d.ts.map +1 -1
- package/dist/task-feed.d.ts +3 -0
- package/dist/task-feed.d.ts.map +1 -1
- package/dist/thinking-option.d.ts +33 -5
- package/dist/thinking-option.d.ts.map +1 -1
- package/dist/thinking-option.js +47 -10
- package/dist/tools.d.ts +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/ultracode.d.ts +1 -1
- package/dist/ultracode.d.ts.map +1 -1
- package/dist/usage-markdown.d.ts.map +1 -1
- package/dist/usage-markdown.js +74 -4
- package/package.json +4 -4
package/dist/acp-agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { agent as acpAgent, methods, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk";
|
|
2
|
-
import { deleteSession, getSessionMessages, listSessions, query, } from "@anthropic-ai/claude-agent-sdk";
|
|
1
|
+
import { agent as acpAgent, methods, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk/unstable";
|
|
2
|
+
import { deleteSession, getSessionMessages, getSubagentMessages, listSessions, query, } 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 { sanitizeTitle, SessionTitles } from "./session-titles.js";
|
|
5
5
|
import { readResumedPermissionMode, readResumedSession, } from "./resumed-session.js";
|
|
@@ -18,7 +18,7 @@ import { filterDeprecatedModels } from "./model-deprecation.js";
|
|
|
18
18
|
import { filterSupersededModels } from "./model-recency.js";
|
|
19
19
|
import { SettingsManager } from "./settings.js";
|
|
20
20
|
import { ContextCompactionLifecycle, contextCompactionMetadataFromBoundary, } from "./context-compaction.js";
|
|
21
|
-
import { createThinkingConfigOption, effectiveThinkingConfig, resolveThinkingSelection, THINKING_CONFIG_ID, } from "./thinking-option.js";
|
|
21
|
+
import { createThinkingConfigOption, effectiveThinkingConfig, modelCanDisableThinking, resolveThinkingSelection, THINKING_CONFIG_ID, } from "./thinking-option.js";
|
|
22
22
|
import { applyLegacyUltracodeEffort, applyUltracodeReadback, applyUltracodeToggle, effortFlagSettings, isLegacyUltracodeEffort, isUltracodeAvailable, readUltracodeState, resolveUltracodeRequested, ULTRACODE_CONFIG_ID, ultracodeConfigOption, } from "./ultracode.js";
|
|
23
23
|
import { deleteSessionIfStored } from "./session-delete.js";
|
|
24
24
|
import { applyPlanUpdatesToOptions, createPlanUpdatesConfigOption, resolvePlanUpdatesAtSpawn, resolvePlanUpdatesRequested, TODO_TOOLS_ENV, } from "./plan-updates.js";
|
|
@@ -36,6 +36,9 @@ import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./u
|
|
|
36
36
|
import { acceptedPlanToolResult, ExitPlanCoordinator, exitPlanModeRawOutput, isExitPlanInterruptionResult, observeExitPlanToolResults, } from "./exit-plan.js";
|
|
37
37
|
import { DEFAULT_AGENT_ID, EFFORT_CONFIG_ID, PLAN_UPDATES_CONFIG_ID, } from "./session-config-ids.js";
|
|
38
38
|
import { withPrecompactHistory } from "./compacted-history.js";
|
|
39
|
+
import { subagentForm } from "./acp-subagents.js";
|
|
40
|
+
import { ChildSessionRouter, childStopReasonOf } from "./child-sessions.js";
|
|
41
|
+
import { readSubagentLaunches } from "./child-session-history.js";
|
|
39
42
|
import { findTranscript } from "./transcript-path.js";
|
|
40
43
|
import { parseToolResultMeta } from "./tool-result-meta.js";
|
|
41
44
|
import { AccountUsageTracker } from "./account-usage.js";
|
|
@@ -157,6 +160,9 @@ export const TASKS_OUTPUT_METHOD = "_claude/tasks/output";
|
|
|
157
160
|
export const TASKS_CAPABILITY_KEY = TASKS_META_KEY;
|
|
158
161
|
/** Version 2 (story 041) adds `hasOutput`, `tail` and `_claude/tasks/output`. */
|
|
159
162
|
export const TASKS_CAPABILITY_VERSION = TASKS_CONTRACT_VERSION;
|
|
163
|
+
/** Version 3: a subagent entry names its child session (`childSessionId`).
|
|
164
|
+
* Only a client on the subagents RFD gets it (story 065, R6.2, R6.3). */
|
|
165
|
+
export const TASKS_RFD_CONTRACT_VERSION = 3;
|
|
160
166
|
/** Refresh the account quota windows of one session now, on demand. Params
|
|
161
167
|
* {@link UsageRefreshRequest}, result {@link UsageRefreshResponse}. The windows
|
|
162
168
|
* themselves arrive the way they always do — one `usage_update` per window,
|
|
@@ -400,6 +406,69 @@ function stripSubagentTextAndThinking(content) {
|
|
|
400
406
|
}
|
|
401
407
|
/** A cached tool call's `description` input, when it is a string — or, for a
|
|
402
408
|
* shell call, its `command_description` alias (CLI 2.1.295+, upstream #1330). */
|
|
409
|
+
/** What a child session is announced with, read from `task_started` and the
|
|
410
|
+
* Agent call it names (story 065, R2.1): the Agent call's `name` or the
|
|
411
|
+
* subagent type as the title, its short description, and its prompt. */
|
|
412
|
+
function subagentLaunchOf(parentSessionId, session, message) {
|
|
413
|
+
const input = (session.toolUseCache[message.tool_use_id ?? ""]?.input ?? {});
|
|
414
|
+
const name = typeof input.name === "string" && input.name.trim() ? input.name : undefined;
|
|
415
|
+
const prompt = message.prompt ?? (typeof input.prompt === "string" ? input.prompt : undefined);
|
|
416
|
+
return {
|
|
417
|
+
parentSessionId,
|
|
418
|
+
launcherTaskId: message.parent_task_id,
|
|
419
|
+
toolUseId: message.tool_use_id,
|
|
420
|
+
agentId: message.task_id,
|
|
421
|
+
title: name ?? message.subagent_type ?? message.description,
|
|
422
|
+
description: message.description,
|
|
423
|
+
prompt,
|
|
424
|
+
cancellable: typeof session.query.stopTask === "function",
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/** A child read back from a transcript, in the shape `ChildSessionRouter`
|
|
428
|
+
* announces: the same title rule as a live launch (`subagentLaunchOf`). */
|
|
429
|
+
function childLaunchOf(parentSessionId, session, toolUseId, record, input) {
|
|
430
|
+
const text = (value) => (typeof value === "string" && value.trim() ? value : undefined);
|
|
431
|
+
const description = record.description ?? text(input.description) ?? "";
|
|
432
|
+
return {
|
|
433
|
+
parentSessionId,
|
|
434
|
+
launcherTaskId: record.launcherAgentId,
|
|
435
|
+
toolUseId,
|
|
436
|
+
agentId: record.agentId,
|
|
437
|
+
title: record.name ?? text(input.name) ?? record.agentType ?? (description || "Subagent"),
|
|
438
|
+
description,
|
|
439
|
+
prompt: text(input.prompt),
|
|
440
|
+
// History, not a task the CLI holds: no Stop until it runs again, when its
|
|
441
|
+
// live launch advertises what the SDK can stop (design.md, Security).
|
|
442
|
+
cancellable: false,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/** The text of a user message whose content is a string or one text block. */
|
|
446
|
+
function userText(content) {
|
|
447
|
+
if (typeof content === "string")
|
|
448
|
+
return content;
|
|
449
|
+
if (Array.isArray(content) && content.length === 1 && content[0]?.type === "text") {
|
|
450
|
+
return content[0].text;
|
|
451
|
+
}
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
/** A child's permission request ended by `session/cancel` on that child. */
|
|
455
|
+
class ChildSessionCancelled extends Error {
|
|
456
|
+
constructor(childId) {
|
|
457
|
+
super(`Subagent ${childId} was cancelled`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/** `client` with every `session/update` passed through the child router. */
|
|
461
|
+
function withChildRouting(client, router) {
|
|
462
|
+
return new Proxy(client, {
|
|
463
|
+
get(target, property) {
|
|
464
|
+
if (property === "sessionUpdate") {
|
|
465
|
+
return (notification) => target.sessionUpdate(router.route(notification));
|
|
466
|
+
}
|
|
467
|
+
const value = Reflect.get(target, property, target);
|
|
468
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
}
|
|
403
472
|
function toolDescriptionOf(cache, toolUseId) {
|
|
404
473
|
if (typeof toolUseId !== "string")
|
|
405
474
|
return undefined;
|
|
@@ -772,6 +841,12 @@ export class ClaudeAcpAgent {
|
|
|
772
841
|
sessions;
|
|
773
842
|
client;
|
|
774
843
|
clientCapabilities;
|
|
844
|
+
/** Set at `initialize`: `rfd` when the client declared `clientCapabilities.subagents`. */
|
|
845
|
+
subagentForm;
|
|
846
|
+
/** Present only in the RFD form: moves subagent traffic to child sessions (story 065). */
|
|
847
|
+
childSessions;
|
|
848
|
+
/** Open permission requests of each child, aborted when that child is cancelled. */
|
|
849
|
+
childAsks = new Map();
|
|
775
850
|
logger;
|
|
776
851
|
sessionModes;
|
|
777
852
|
gatewayAuthRequest;
|
|
@@ -843,6 +918,16 @@ export class ClaudeAcpAgent {
|
|
|
843
918
|
}
|
|
844
919
|
async initialize(request) {
|
|
845
920
|
this.clientCapabilities = request.clientCapabilities;
|
|
921
|
+
this.subagentForm = subagentForm(request.clientCapabilities);
|
|
922
|
+
if (this.subagentForm === "rfd" && !this.childSessions) {
|
|
923
|
+
const direct = this.client;
|
|
924
|
+
const router = new ChildSessionRouter((notification) => direct.sessionUpdate(notification), (line) => this.logger.log(line));
|
|
925
|
+
this.childSessions = router;
|
|
926
|
+
// The one place every `session/update` crosses — the consumer's
|
|
927
|
+
// `sendUpdate`, the task feed, hooks, the classifier — so a subagent's
|
|
928
|
+
// update cannot reach the parent by an emission site that forgot it.
|
|
929
|
+
this.client = withChildRouting(direct, router);
|
|
930
|
+
}
|
|
846
931
|
// Bypasses standard auth by routing requests through a custom Anthropic-protocol gateway.
|
|
847
932
|
// Only offered when the client advertises `auth._meta.gateway` capability.
|
|
848
933
|
const supportsGatewayAuth = request.clientCapabilities?.auth?._meta?.gateway === true;
|
|
@@ -945,7 +1030,9 @@ export class ClaudeAcpAgent {
|
|
|
945
1030
|
claudeCode: {
|
|
946
1031
|
promptQueueing: true,
|
|
947
1032
|
},
|
|
948
|
-
[TASKS_CAPABILITY_KEY]: {
|
|
1033
|
+
[TASKS_CAPABILITY_KEY]: {
|
|
1034
|
+
version: this.childSessions ? TASKS_RFD_CONTRACT_VERSION : TASKS_CAPABILITY_VERSION,
|
|
1035
|
+
},
|
|
949
1036
|
[USAGE_CAPABILITY_KEY]: { version: USAGE_CAPABILITY_VERSION },
|
|
950
1037
|
},
|
|
951
1038
|
promptCapabilities: {
|
|
@@ -1030,9 +1117,11 @@ export class ClaudeAcpAgent {
|
|
|
1030
1117
|
return response;
|
|
1031
1118
|
}
|
|
1032
1119
|
async resumeSession(params) {
|
|
1120
|
+
this.refuseChildSession(params.sessionId, "lifecycle");
|
|
1033
1121
|
if (this.providerUpdate)
|
|
1034
1122
|
await this.providerUpdate;
|
|
1035
1123
|
const result = await this.getOrCreateSession(params);
|
|
1124
|
+
await this.restoreChildSessions(params.sessionId);
|
|
1036
1125
|
// Needs to happen after we return the session
|
|
1037
1126
|
setTimeout(() => {
|
|
1038
1127
|
this.sendAvailableCommandsUpdate(params.sessionId);
|
|
@@ -1040,6 +1129,7 @@ export class ClaudeAcpAgent {
|
|
|
1040
1129
|
return result;
|
|
1041
1130
|
}
|
|
1042
1131
|
async loadSession(params) {
|
|
1132
|
+
this.refuseChildSession(params.sessionId, "lifecycle");
|
|
1043
1133
|
if (this.providerUpdate)
|
|
1044
1134
|
await this.providerUpdate;
|
|
1045
1135
|
const result = await this.getOrCreateSession(params);
|
|
@@ -1673,6 +1763,37 @@ export class ClaudeAcpAgent {
|
|
|
1673
1763
|
* `session/prompt`. Without the opt-in, the existing detached `prompt()` and
|
|
1674
1764
|
* `startedNewTurn` result are preserved for compatibility. */
|
|
1675
1765
|
/** `_claude/tasks/stop`: stop a task the feed holds as running (R2.1, R2.2). */
|
|
1766
|
+
/** `_claude/tasks` v3: each subagent entry with a child session names it
|
|
1767
|
+
* (R6.1); every other entry, and every snapshot off the RFD, is the v2
|
|
1768
|
+
* object as is (R6.3, R6.4). */
|
|
1769
|
+
withChildSessionIds(snapshot) {
|
|
1770
|
+
const router = this.childSessions;
|
|
1771
|
+
if (!router)
|
|
1772
|
+
return snapshot;
|
|
1773
|
+
return {
|
|
1774
|
+
...snapshot,
|
|
1775
|
+
tasks: snapshot.tasks.map((task) => task.type === "subagent" && router.isChild(task.id)
|
|
1776
|
+
? { ...task, childSessionId: task.id }
|
|
1777
|
+
: task),
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
/** A child session is the parent's: its config is read-only, and it is
|
|
1781
|
+
* loaded, resumed and closed with its parent (story 065, R5.1, R5.2). Checked
|
|
1782
|
+
* first, so an id that names no session keeps the error it has today. */
|
|
1783
|
+
refuseChildSession(sessionId, operation) {
|
|
1784
|
+
if (!this.childSessions?.isChild(sessionId))
|
|
1785
|
+
return;
|
|
1786
|
+
throw RequestError.invalidParams({ sessionId }, operation === "config"
|
|
1787
|
+
? "subagent sessions are read-only and loaded with their parent"
|
|
1788
|
+
: "subagent sessions are loaded with their parent and cannot be loaded, resumed or closed alone");
|
|
1789
|
+
}
|
|
1790
|
+
/** A subagent task ended: its child, if it has one, goes idle (story 065, R2.4). */
|
|
1791
|
+
async settleChildSession(taskId, status) {
|
|
1792
|
+
const stopReason = childStopReasonOf(status);
|
|
1793
|
+
if (!stopReason || !this.childSessions?.isChild(taskId))
|
|
1794
|
+
return;
|
|
1795
|
+
await this.childSessions.settle(taskId, stopReason);
|
|
1796
|
+
}
|
|
1676
1797
|
async stopTask(params) {
|
|
1677
1798
|
const { sessionId, taskId } = params;
|
|
1678
1799
|
return this.taskAction(TASKS_STOP_METHOD, sessionId, `taskId=${logValue(taskId)}`, async (session) => {
|
|
@@ -3239,6 +3360,17 @@ export class ClaudeAcpAgent {
|
|
|
3239
3360
|
this.logger.error(`Session ${message.session_id}: failed to persist history: ${message.error}`);
|
|
3240
3361
|
break;
|
|
3241
3362
|
}
|
|
3363
|
+
case "session_writes_lost": {
|
|
3364
|
+
// SDK 0.3.317+: the CLI could not write its transcript or
|
|
3365
|
+
// ~/.claude.json because the disk (or quota) was full. Same
|
|
3366
|
+
// class as mirror_error — lost session data a human must act
|
|
3367
|
+
// on — so it is logged the same way; the wire is unchanged.
|
|
3368
|
+
this.logger.error(`Session ${message.session_id}: session writes lost (${message.error_code}): ${(message.lost ?? []).join(", ")}` +
|
|
3369
|
+
(message.transcript_session_ids?.length
|
|
3370
|
+
? `; transcripts: ${message.transcript_session_ids.join(", ")}`
|
|
3371
|
+
: ""));
|
|
3372
|
+
break;
|
|
3373
|
+
}
|
|
3242
3374
|
case "permission_denied": {
|
|
3243
3375
|
// A tool call was auto-denied (by a rule, the classifier,
|
|
3244
3376
|
// dontAsk mode, etc.) before running. The tool_use block was
|
|
@@ -3402,6 +3534,9 @@ export class ClaudeAcpAgent {
|
|
|
3402
3534
|
if (message.subagent_type && session.activeTurn && !session.activeTurn.settled) {
|
|
3403
3535
|
(session.activeTurn.spawnedTaskIds ??= new Set()).add(message.task_id);
|
|
3404
3536
|
}
|
|
3537
|
+
if (this.childSessions && message.subagent_type && message.tool_use_id) {
|
|
3538
|
+
await this.childSessions.launch(subagentLaunchOf(params.sessionId, session, message));
|
|
3539
|
+
}
|
|
3405
3540
|
// The spawning tool call's own `description` labels a shell
|
|
3406
3541
|
// task instead of its command line. Read it now: the cache
|
|
3407
3542
|
// entry is pruned at `tool_result`, which for a background
|
|
@@ -3416,6 +3551,7 @@ export class ClaudeAcpAgent {
|
|
|
3416
3551
|
// The task settled — no further tool calls can originate
|
|
3417
3552
|
// from it, so its registry entry can be dropped.
|
|
3418
3553
|
session.liveBackgroundTasks.delete(message.task_id);
|
|
3554
|
+
await this.settleChildSession(message.task_id, message.status);
|
|
3419
3555
|
await endBackgroundTerminal(message.task_id, backgroundExitCode(message.status, message.summary));
|
|
3420
3556
|
armDrainGrace();
|
|
3421
3557
|
if (session.taskFeed && session.taskFeedPublisher) {
|
|
@@ -3441,6 +3577,7 @@ export class ClaudeAcpAgent {
|
|
|
3441
3577
|
message.patch.status === "failed" ||
|
|
3442
3578
|
message.patch.status === "killed") {
|
|
3443
3579
|
session.liveBackgroundTasks.delete(message.task_id);
|
|
3580
|
+
await this.settleChildSession(message.task_id, message.patch.status);
|
|
3444
3581
|
// Usually first: the notification with its summary follows
|
|
3445
3582
|
// but is not guaranteed, so the terminal ends here.
|
|
3446
3583
|
await endBackgroundTerminal(message.task_id, backgroundExitCode(message.patch.status, message.patch.error));
|
|
@@ -4378,7 +4515,11 @@ export class ClaudeAcpAgent {
|
|
|
4378
4515
|
break;
|
|
4379
4516
|
}
|
|
4380
4517
|
}
|
|
4381
|
-
|
|
4518
|
+
// A parent cancel keeps its background subagents running (story
|
|
4519
|
+
// 065, R3.2): in the RFD form their stream goes on to their child
|
|
4520
|
+
// sessions even while the parent's cancelled turn drains.
|
|
4521
|
+
if (session.cancelled &&
|
|
4522
|
+
!this.childSessions?.childFor(params.sessionId, parentToolUseIdOf(message))) {
|
|
4382
4523
|
break;
|
|
4383
4524
|
}
|
|
4384
4525
|
// Synthetic assistant frames carry the CLI's local-command output.
|
|
@@ -4581,7 +4722,11 @@ export class ClaudeAcpAgent {
|
|
|
4581
4722
|
streamedBlocks.length = 0;
|
|
4582
4723
|
}
|
|
4583
4724
|
else if (message.type === "assistant" &&
|
|
4584
|
-
!(session.forwardSubagentText ||
|
|
4725
|
+
!(session.forwardSubagentText ||
|
|
4726
|
+
supportsSubagentTranscript(this.clientCapabilities) ||
|
|
4727
|
+
// A child session is the subagent's own transcript (story 065):
|
|
4728
|
+
// its text belongs there, never in the parent's feed.
|
|
4729
|
+
this.childSessions?.childFor(params.sessionId, message.parent_tool_use_id))) {
|
|
4585
4730
|
// Legacy clients don't understand nested transcripts. Keep the
|
|
4586
4731
|
// historical behavior for them: subagent text/thinking remains
|
|
4587
4732
|
// internal to the tool call instead of leaking into the top-level
|
|
@@ -4833,8 +4978,59 @@ export class ClaudeAcpAgent {
|
|
|
4833
4978
|
}
|
|
4834
4979
|
}
|
|
4835
4980
|
async cancel(params) {
|
|
4981
|
+
if (this.childSessions?.isChild(params.sessionId)) {
|
|
4982
|
+
await this.cancelChildSession(params.sessionId);
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4836
4985
|
await this.cancelTurns(params, { awaitInterrupt: true });
|
|
4837
4986
|
}
|
|
4987
|
+
/** Register an ask (permission or question) of `childId`, aborted when that
|
|
4988
|
+
* child is cancelled; already aborted when it was. No-op for the parent. */
|
|
4989
|
+
openChildAsk(childId) {
|
|
4990
|
+
const controller = new AbortController();
|
|
4991
|
+
if (!childId)
|
|
4992
|
+
return controller;
|
|
4993
|
+
if (this.childSessions.isCancelled(childId)) {
|
|
4994
|
+
controller.abort();
|
|
4995
|
+
return controller;
|
|
4996
|
+
}
|
|
4997
|
+
let asks = this.childAsks.get(childId);
|
|
4998
|
+
if (!asks)
|
|
4999
|
+
this.childAsks.set(childId, (asks = new Set()));
|
|
5000
|
+
asks.add(controller);
|
|
5001
|
+
return controller;
|
|
5002
|
+
}
|
|
5003
|
+
closeChildAsk(childId, controller) {
|
|
5004
|
+
if (childId)
|
|
5005
|
+
this.childAsks.get(childId)?.delete(controller);
|
|
5006
|
+
}
|
|
5007
|
+
/** `session/cancel` naming a child stops that subagent alone: its SDK task,
|
|
5008
|
+
* never the parent's query, and its open questions are answered denied
|
|
5009
|
+
* (story 065, R3.1, R3.3). On a failed stop the state stays as reported. */
|
|
5010
|
+
async cancelChildSession(childId) {
|
|
5011
|
+
const parentId = this.childSessions.parentOf(childId);
|
|
5012
|
+
const session = this.sessions[parentId];
|
|
5013
|
+
if (!session) {
|
|
5014
|
+
// The parent is gone, and its query with it: nothing is left to stop.
|
|
5015
|
+
this.logger.log(`[subagents/cancel] outcome=parent_gone sessionId=${logValue(parentId)} childSessionId=${logValue(childId)}`);
|
|
5016
|
+
await this.childSessions.settle(childId, "cancelled");
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
// Join a query recreate, as `cancelTurns` does, so the stop reaches the live query.
|
|
5020
|
+
if (session.queryRecreateInFlight)
|
|
5021
|
+
await session.queryRecreateInFlight;
|
|
5022
|
+
try {
|
|
5023
|
+
await session.query.stopTask(childId);
|
|
5024
|
+
}
|
|
5025
|
+
catch (error) {
|
|
5026
|
+
this.logger.log(`[subagents/cancel] outcome=failed level=warn sessionId=${logValue(parentId)} childSessionId=${logValue(childId)} reason=${logValue(errorMessage(error))}`);
|
|
5027
|
+
throw new Error(`Could not stop subagent ${childId}`, { cause: error });
|
|
5028
|
+
}
|
|
5029
|
+
for (const ask of this.childAsks.get(childId) ?? [])
|
|
5030
|
+
ask.abort();
|
|
5031
|
+
this.childAsks.delete(childId);
|
|
5032
|
+
await this.childSessions.settle(childId, "cancelled");
|
|
5033
|
+
}
|
|
4838
5034
|
/** Cancel the session's turns and interrupt the SDK query. With
|
|
4839
5035
|
* `awaitInterrupt: false` the interrupt is sent, but its reply is not
|
|
4840
5036
|
* awaited. `teardownSession` uses that: it closes the query right after, and
|
|
@@ -5146,7 +5342,10 @@ export class ClaudeAcpAgent {
|
|
|
5146
5342
|
{ warn: (message) => this.logger.log(`${message} level=warn`) }, sessionId);
|
|
5147
5343
|
const taskFeedPublisher = new TaskFeedPublisher(taskFeed, (snapshot) => this.client.sessionUpdate({
|
|
5148
5344
|
sessionId,
|
|
5149
|
-
update: {
|
|
5345
|
+
update: {
|
|
5346
|
+
sessionUpdate: "session_info_update",
|
|
5347
|
+
_meta: { [TASKS_META_KEY]: this.withChildSessionIds(snapshot) },
|
|
5348
|
+
},
|
|
5150
5349
|
}), undefined, (error) => this.logger.error(`[tasks/feed] sessionId=${logValue(sessionId)} outcome=send_failed reason=${logValue(errorMessage(error))}`));
|
|
5151
5350
|
const tailPoller = new TailPoller(taskFeed, taskFeedPublisher, undefined, undefined, (line) => this.logger.log(line), sessionId);
|
|
5152
5351
|
return { taskFeed, taskFeedPublisher, tailPoller };
|
|
@@ -5189,6 +5388,7 @@ export class ClaudeAcpAgent {
|
|
|
5189
5388
|
await Promise.all(Object.keys(this.sessions).map((id) => this.teardownSession(id)));
|
|
5190
5389
|
}
|
|
5191
5390
|
async closeSession(params) {
|
|
5391
|
+
this.refuseChildSession(params.sessionId, "lifecycle");
|
|
5192
5392
|
if (!this.sessions[params.sessionId]) {
|
|
5193
5393
|
throw new Error("Session not found");
|
|
5194
5394
|
}
|
|
@@ -5207,6 +5407,7 @@ export class ClaudeAcpAgent {
|
|
|
5207
5407
|
return {};
|
|
5208
5408
|
}
|
|
5209
5409
|
async setSessionMode(params) {
|
|
5410
|
+
this.refuseChildSession(params.sessionId, "config");
|
|
5210
5411
|
// A lazy Thinking recreate may be swapping the session's query right now;
|
|
5211
5412
|
// join it (mirroring prompt()) so `setPermissionMode` below lands on the
|
|
5212
5413
|
// replacement query after cutover instead of an about-to-close one — and
|
|
@@ -5222,6 +5423,7 @@ export class ClaudeAcpAgent {
|
|
|
5222
5423
|
return this.sessionModes.setSessionMode(params);
|
|
5223
5424
|
}
|
|
5224
5425
|
async setSessionConfigOption(params) {
|
|
5426
|
+
this.refuseChildSession(params.sessionId, "config");
|
|
5225
5427
|
const session = this.sessions[params.sessionId];
|
|
5226
5428
|
if (!session) {
|
|
5227
5429
|
throw new Error("Session not found");
|
|
@@ -5284,12 +5486,20 @@ export class ClaudeAcpAgent {
|
|
|
5284
5486
|
// refreshed and acked immediately, mirroring Fast mode. An unrecognized
|
|
5285
5487
|
// value deliberately falls through to the shared invalid-option-value
|
|
5286
5488
|
// validation below rather than growing a bespoke error shape.
|
|
5489
|
+
//
|
|
5490
|
+
// An "off" on a model that cannot turn thinking off is still kept as the
|
|
5491
|
+
// intent rather than refused (story 071, R2.2): a client holding the option
|
|
5492
|
+
// list from before a model switch gets no error, the option reads "on"
|
|
5493
|
+
// there and the query runs adaptive (`effectiveThinkingConfig`), and the
|
|
5494
|
+
// retained Off holds again on a model that can honour it. (Zed itself
|
|
5495
|
+
// drops a saved default the option does not list, so it never sends one.)
|
|
5287
5496
|
if (params.configId === THINKING_CONFIG_ID) {
|
|
5288
5497
|
const enabled = resolveThinkingSelection(params.value);
|
|
5498
|
+
const canDisable = modelCanDisableThinking(session.modelInfos, session.models.currentModelId);
|
|
5289
5499
|
if (enabled !== null) {
|
|
5290
5500
|
session.thinkingEnabled = enabled;
|
|
5291
5501
|
session.pendingQueryRecreate = true;
|
|
5292
|
-
session.configOptions = session.configOptions.map((o) => o.id === THINKING_CONFIG_ID ? createThinkingConfigOption(enabled) : o);
|
|
5502
|
+
session.configOptions = session.configOptions.map((o) => o.id === THINKING_CONFIG_ID ? createThinkingConfigOption(enabled, canDisable) : o);
|
|
5293
5503
|
return { configOptions: session.configOptions };
|
|
5294
5504
|
}
|
|
5295
5505
|
}
|
|
@@ -5356,8 +5566,99 @@ export class ClaudeAcpAgent {
|
|
|
5356
5566
|
await this.applyConfigOptionValue(params.sessionId, session, params.configId, effectiveValue);
|
|
5357
5567
|
return { configOptions: session.configOptions };
|
|
5358
5568
|
}
|
|
5569
|
+
/** A resumed parent's children, registered under their transcript ids but
|
|
5570
|
+
* not announced: a resume replays nothing, and a child the parent
|
|
5571
|
+
* continues is announced when it runs (story 065, R4.3). */
|
|
5572
|
+
async restoreChildSessions(sessionId) {
|
|
5573
|
+
if (!this.childSessions)
|
|
5574
|
+
return;
|
|
5575
|
+
const session = this.sessions[sessionId];
|
|
5576
|
+
const launches = await readSubagentLaunches(CLAUDE_CONFIG_DIR, sessionId);
|
|
5577
|
+
// Launchers before the subagents they launched, so each nests under its own.
|
|
5578
|
+
const byDepth = [...launches].sort(([, a], [, b]) => a.depth - b.depth);
|
|
5579
|
+
for (const [toolUseId, record] of byDepth) {
|
|
5580
|
+
await this.childSessions.restore(childLaunchOf(sessionId, session, toolUseId, record, {}), {
|
|
5581
|
+
announce: false,
|
|
5582
|
+
});
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
/** Replay one child of a loaded parent right after the Agent call that
|
|
5586
|
+
* launched it: its announcement and prompt on the parent, then its own
|
|
5587
|
+
* transcript on its session (story 065, R4.1, R4.2). */
|
|
5588
|
+
async replayChildSession(parentId, toolUseId, input, record, launches, replayed) {
|
|
5589
|
+
// Each child once per load: a damaged meta naming a launch inside the
|
|
5590
|
+
// child's own transcript would otherwise recurse without end.
|
|
5591
|
+
if (replayed.has(record.agentId))
|
|
5592
|
+
return;
|
|
5593
|
+
replayed.add(record.agentId);
|
|
5594
|
+
const launch = childLaunchOf(parentId, this.sessions[parentId], toolUseId, record, input);
|
|
5595
|
+
await this.childSessions.restore(launch, { announce: true });
|
|
5596
|
+
if (this.childSessions.parentOf(record.agentId) !== parentId)
|
|
5597
|
+
return;
|
|
5598
|
+
let messages = [];
|
|
5599
|
+
let reason = "empty";
|
|
5600
|
+
try {
|
|
5601
|
+
messages = await getSubagentMessages(parentId, record.agentId);
|
|
5602
|
+
}
|
|
5603
|
+
catch (error) {
|
|
5604
|
+
reason = errorMessage(error);
|
|
5605
|
+
}
|
|
5606
|
+
if (messages.length === 0) {
|
|
5607
|
+
// The SDK answers a missing transcript with []: only the association replays.
|
|
5608
|
+
this.logger.log(`[subagents/replay] outcome=no_transcript level=warn sessionId=${logValue(parentId)} childSessionId=${logValue(record.agentId)} reason=${logValue(reason)}`);
|
|
5609
|
+
return;
|
|
5610
|
+
}
|
|
5611
|
+
const toolUseCache = {};
|
|
5612
|
+
let promptSkipped = false;
|
|
5613
|
+
for (const message of messages) {
|
|
5614
|
+
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
5615
|
+
const role = message.message?.role;
|
|
5616
|
+
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
5617
|
+
let content = message.message?.content;
|
|
5618
|
+
if (role === "user") {
|
|
5619
|
+
// The launch prompt already went out as the parent's session_message.
|
|
5620
|
+
if (!promptSkipped && launch.prompt && userText(content) === launch.prompt) {
|
|
5621
|
+
promptSkipped = true;
|
|
5622
|
+
continue;
|
|
5623
|
+
}
|
|
5624
|
+
content = stripLocalCommandMetadata(content);
|
|
5625
|
+
if (content === null)
|
|
5626
|
+
continue;
|
|
5627
|
+
}
|
|
5628
|
+
for (const notification of toAcpNotifications(
|
|
5629
|
+
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
5630
|
+
content, role, parentId, toolUseCache, this.client, this.logger, {
|
|
5631
|
+
registerHooks: false,
|
|
5632
|
+
clientCapabilities: this.clientCapabilities,
|
|
5633
|
+
cwd: this.sessions[parentId]?.cwd,
|
|
5634
|
+
taskState: this.sessions[parentId]?.taskState,
|
|
5635
|
+
messageId: messageIdForGrouping(message),
|
|
5636
|
+
})) {
|
|
5637
|
+
await this.client.sessionUpdate({ ...notification, sessionId: record.agentId });
|
|
5638
|
+
}
|
|
5639
|
+
// A nested subagent's launch lives in its launcher's transcript.
|
|
5640
|
+
if (role === "assistant") {
|
|
5641
|
+
await this.replayLaunchesIn(parentId, content, launches, replayed);
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
}
|
|
5645
|
+
/** Replay the children launched by the Agent calls in `content`. */
|
|
5646
|
+
async replayLaunchesIn(parentId, content, launches, replayed) {
|
|
5647
|
+
for (const block of Array.isArray(content) ? content : []) {
|
|
5648
|
+
const record = block?.type === "tool_use" && typeof block.id === "string"
|
|
5649
|
+
? launches.get(block.id)
|
|
5650
|
+
: undefined;
|
|
5651
|
+
if (record) {
|
|
5652
|
+
await this.replayChildSession(parentId, block.id, block.input ?? {}, record, launches, replayed);
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5359
5656
|
async replaySessionHistory(sessionId) {
|
|
5360
5657
|
const toolUseCache = {};
|
|
5658
|
+
const childLaunches = this.childSessions
|
|
5659
|
+
? await readSubagentLaunches(CLAUDE_CONFIG_DIR, sessionId)
|
|
5660
|
+
: undefined;
|
|
5661
|
+
const replayedChildren = new Set();
|
|
5361
5662
|
// getSessionMessages stops at the last compaction boundary; put the history
|
|
5362
5663
|
// before it back, so a reopened compacted thread scrolls to its first prompt.
|
|
5363
5664
|
const messages = await withPrecompactHistory(sessionId, await getSessionMessages(sessionId), CLAUDE_CONFIG_DIR);
|
|
@@ -5500,6 +5801,9 @@ export class ClaudeAcpAgent {
|
|
|
5500
5801
|
})) {
|
|
5501
5802
|
await this.client.sessionUpdate(notification);
|
|
5502
5803
|
}
|
|
5804
|
+
if (childLaunches?.size && message.type === "assistant" && parentToolUseId === null) {
|
|
5805
|
+
await this.replayLaunchesIn(sessionId, content, childLaunches, replayedChildren);
|
|
5806
|
+
}
|
|
5503
5807
|
}
|
|
5504
5808
|
}
|
|
5505
5809
|
async readTextFile(params) {
|
|
@@ -5551,21 +5855,44 @@ export class ClaudeAcpAgent {
|
|
|
5551
5855
|
// so emit it now if it hasn't been sent yet. The streamed tool_use chunk
|
|
5552
5856
|
// later refines it with a `tool_call_update` rather than emitting a
|
|
5553
5857
|
// duplicate (see `emittedToolCalls` in `toAcpNotifications`).
|
|
5858
|
+
// A child's ask is registered before anything awaits, so a cancel of
|
|
5859
|
+
// that child landing meanwhile still answers it (story 065, R3.3).
|
|
5860
|
+
const childId = this.childSessions?.childFor(params.sessionId, parentToolUseId);
|
|
5861
|
+
const childCancel = this.openChildAsk(childId);
|
|
5554
5862
|
await this.ensureToolCallEmitted(params.sessionId, toolName, params.toolCall.toolCallId, params.toolCall.rawInput, parentToolUseId, signal);
|
|
5555
|
-
if (signal.aborted)
|
|
5863
|
+
if (signal.aborted) {
|
|
5864
|
+
this.closeChildAsk(childId, childCancel);
|
|
5556
5865
|
throw new Error("Tool use aborted");
|
|
5866
|
+
}
|
|
5557
5867
|
// Do not rely on every ACP client settling requestPermission after the
|
|
5558
5868
|
// cancellation signal. The local race guarantees that Claude's tool call
|
|
5559
5869
|
// is released even when an older or broken client ignores $/cancel_request.
|
|
5870
|
+
const outgoing = childId ? { ...params, sessionId: childId } : params;
|
|
5871
|
+
const requestSignal = childId ? AbortSignal.any([signal, childCancel.signal]) : signal;
|
|
5872
|
+
if (childId) {
|
|
5873
|
+
if (childCancel.signal.aborted) {
|
|
5874
|
+
this.closeChildAsk(childId, childCancel);
|
|
5875
|
+
throw new ChildSessionCancelled(childId);
|
|
5876
|
+
}
|
|
5877
|
+
await this.childSessions.askOpened(childId);
|
|
5878
|
+
}
|
|
5560
5879
|
try {
|
|
5561
|
-
return await this.withPendingUserInput(params.sessionId, () => raceWithAbort(this.client.requestPermission(
|
|
5880
|
+
return await this.withPendingUserInput(params.sessionId, () => raceWithAbort(this.client.requestPermission(outgoing, requestSignal), requestSignal));
|
|
5562
5881
|
}
|
|
5563
5882
|
catch (error) {
|
|
5564
5883
|
if (signal.aborted) {
|
|
5565
5884
|
throw new Error("Tool use aborted", { cause: error });
|
|
5566
5885
|
}
|
|
5886
|
+
if (childCancel.signal.aborted)
|
|
5887
|
+
throw new ChildSessionCancelled(childId);
|
|
5567
5888
|
throw error;
|
|
5568
5889
|
}
|
|
5890
|
+
finally {
|
|
5891
|
+
if (childId) {
|
|
5892
|
+
this.closeChildAsk(childId, childCancel);
|
|
5893
|
+
await this.childSessions.askClosed(childId);
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5569
5896
|
}
|
|
5570
5897
|
/** Emit the `tool_call` a permission request references if it hasn't been sent
|
|
5571
5898
|
* yet, so the client has the tool call before being asked to approve it. The
|
|
@@ -5666,7 +5993,7 @@ export class ClaudeAcpAgent {
|
|
|
5666
5993
|
// Like permission requests, the elicitation references this toolUseID, so
|
|
5667
5994
|
// make sure the tool_call has surfaced to the client before we send it.
|
|
5668
5995
|
await this.ensureToolCallEmitted(sessionId, toolName, toolUseID, toolInput, parentToolUseId, signal);
|
|
5669
|
-
return this.handleAskUserQuestion(sessionId, toolInput, toolUseID, signal);
|
|
5996
|
+
return this.handleAskUserQuestion(sessionId, toolInput, toolUseID, signal, this.childSessions?.childFor(sessionId, parentToolUseId));
|
|
5670
5997
|
}
|
|
5671
5998
|
// Do not auto-allow here based on the session's advertised mode. Claude
|
|
5672
5999
|
// Code applies bypassPermissions before invoking canUseTool; a request
|
|
@@ -5713,11 +6040,20 @@ export class ClaudeAcpAgent {
|
|
|
5713
6040
|
? undefined
|
|
5714
6041
|
: Math.max(0, Math.min(100, Math.round((session.contextUsedTokens / session.contextWindowSize) * 100))),
|
|
5715
6042
|
});
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
6043
|
+
let response;
|
|
6044
|
+
try {
|
|
6045
|
+
response = await this.requestPermissionFromClient({
|
|
6046
|
+
...presentation,
|
|
6047
|
+
options: permissionOptions,
|
|
6048
|
+
sessionId,
|
|
6049
|
+
}, toolName, signal, parentToolUseId);
|
|
6050
|
+
}
|
|
6051
|
+
catch (error) {
|
|
6052
|
+
if (error instanceof ChildSessionCancelled) {
|
|
6053
|
+
return { behavior: "deny", message: "The subagent was cancelled." };
|
|
6054
|
+
}
|
|
6055
|
+
throw error;
|
|
6056
|
+
}
|
|
5721
6057
|
if (signal.aborted)
|
|
5722
6058
|
throw new Error("Tool use aborted");
|
|
5723
6059
|
const decodedPermission = decodeClaudePermissionResponse(response, toolName, toolInput, toolUseID, permissionOptions, durableChangeSet);
|
|
@@ -5803,17 +6139,31 @@ export class ClaudeAcpAgent {
|
|
|
5803
6139
|
* elicitation and return the answers as the tool's `updatedInput`. Called from
|
|
5804
6140
|
* `canUseTool` since that is where the SDK routes the tool's permission check.
|
|
5805
6141
|
*/
|
|
5806
|
-
async handleAskUserQuestion(sessionId, toolInput, toolUseID, signal
|
|
6142
|
+
async handleAskUserQuestion(sessionId, toolInput, toolUseID, signal,
|
|
6143
|
+
/** A subagent's question goes out on its child session (story 065, R2.3). */
|
|
6144
|
+
childId) {
|
|
5807
6145
|
const questions = extractAskUserQuestions(toolInput);
|
|
5808
6146
|
if (!questions) {
|
|
5809
6147
|
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
5810
6148
|
}
|
|
5811
|
-
const createRequest = askUserQuestionsToCreateRequest(questions, sessionId, toolUseID);
|
|
6149
|
+
const createRequest = askUserQuestionsToCreateRequest(questions, childId ?? sessionId, toolUseID);
|
|
5812
6150
|
let response;
|
|
6151
|
+
const childCancel = this.openChildAsk(childId);
|
|
6152
|
+
if (childCancel.signal.aborted) {
|
|
6153
|
+
return { behavior: "deny", message: "The subagent was cancelled." };
|
|
6154
|
+
}
|
|
6155
|
+
const requestSignal = childId ? AbortSignal.any([signal, childCancel.signal]) : signal;
|
|
6156
|
+
if (childId)
|
|
6157
|
+
await this.childSessions?.askOpened(childId);
|
|
5813
6158
|
try {
|
|
5814
|
-
response = await this.withPendingUserInput(sessionId, () =>
|
|
6159
|
+
response = await this.withPendingUserInput(sessionId, () => childId
|
|
6160
|
+
? raceWithAbort(this.client.unstable_createElicitation(createRequest, requestSignal), requestSignal)
|
|
6161
|
+
: this.client.unstable_createElicitation(createRequest, signal));
|
|
5815
6162
|
}
|
|
5816
6163
|
catch (error) {
|
|
6164
|
+
if (childCancel.signal.aborted && !signal.aborted) {
|
|
6165
|
+
return { behavior: "deny", message: "The subagent was cancelled." };
|
|
6166
|
+
}
|
|
5817
6167
|
// A cancellation we requested (signal aborted) settles as an aborted tool
|
|
5818
6168
|
// use, matching the post-response check below.
|
|
5819
6169
|
if (signal.aborted) {
|
|
@@ -5822,6 +6172,11 @@ export class ClaudeAcpAgent {
|
|
|
5822
6172
|
this.logger.error(`Failed to present AskUserQuestion elicitation: ${error}`);
|
|
5823
6173
|
return { behavior: "deny", message: "Could not present the question to the user." };
|
|
5824
6174
|
}
|
|
6175
|
+
finally {
|
|
6176
|
+
this.closeChildAsk(childId, childCancel);
|
|
6177
|
+
if (childId)
|
|
6178
|
+
await this.childSessions?.askClosed(childId);
|
|
6179
|
+
}
|
|
5825
6180
|
if (signal.aborted) {
|
|
5826
6181
|
throw new Error("Tool use aborted");
|
|
5827
6182
|
}
|
|
@@ -5948,6 +6303,14 @@ export class ClaudeAcpAgent {
|
|
|
5948
6303
|
// carries no "1m" token.
|
|
5949
6304
|
const newModelInfo = session.modelInfos.find((m) => m.value === value);
|
|
5950
6305
|
const modelChanged = session.models.currentModelId !== value;
|
|
6306
|
+
// A retained Off is sent as `disabled` or as `adaptive` depending on the
|
|
6307
|
+
// model (story 071): when the switch changes which, the query must be
|
|
6308
|
+
// rebuilt at the next prompt, as a Thinking change itself would.
|
|
6309
|
+
if (session.thinkingEnabled === false &&
|
|
6310
|
+
modelCanDisableThinking(session.modelInfos, session.models.currentModelId) !==
|
|
6311
|
+
modelCanDisableThinking(session.modelInfos, value)) {
|
|
6312
|
+
session.pendingQueryRecreate = true;
|
|
6313
|
+
}
|
|
5951
6314
|
if (modelChanged) {
|
|
5952
6315
|
// Seed the new model's context window WITHOUT awaited IPC on the
|
|
5953
6316
|
// switch path: cached authoritative value if we've already learned it
|
|
@@ -6237,7 +6600,7 @@ export class ClaudeAcpAgent {
|
|
|
6237
6600
|
// tri-state intent beats the env var once set ("off" wins even with
|
|
6238
6601
|
// MAX_THINKING_TOKENS present, R1.5); untouched sessions keep the
|
|
6239
6602
|
// env-driven behavior (R1.6).
|
|
6240
|
-
const thinking = effectiveThinkingConfig(session.thinkingEnabled, process.env.MAX_THINKING_TOKENS, this.logger);
|
|
6603
|
+
const thinking = effectiveThinkingConfig(session.thinkingEnabled, process.env.MAX_THINKING_TOKENS, this.logger, modelCanDisableThinking(session.modelInfos, session.models.currentModelId));
|
|
6241
6604
|
// Snapshot of the live-tracked model/mode the replacement is built
|
|
6242
6605
|
// with. A setter that was ALREADY awaiting its control request on the
|
|
6243
6606
|
// old query when this recreate started (i.e. past its
|
|
@@ -7696,10 +8059,11 @@ planUpdates) {
|
|
|
7696
8059
|
options.push(createFastModeConfigOption(fastMode.enabled, fastMode.useBooleanOption, fastMode.disabledReason));
|
|
7697
8060
|
}
|
|
7698
8061
|
// Surface the Thinking toggle whenever the caller supplies its display
|
|
7699
|
-
// state. Unlike Fast mode
|
|
7700
|
-
//
|
|
8062
|
+
// state. Unlike Fast mode there is no `supported` gate — every session shows
|
|
8063
|
+
// it (R1.1) — but on a model that always thinks it offers "on" alone
|
|
8064
|
+
// (story 071, R2.2).
|
|
7701
8065
|
if (thinkingEnabled !== undefined) {
|
|
7702
|
-
options.push(createThinkingConfigOption(thinkingEnabled));
|
|
8066
|
+
options.push(createThinkingConfigOption(thinkingEnabled, modelCanDisableThinking(modelInfos, models.currentModelId)));
|
|
7703
8067
|
}
|
|
7704
8068
|
// Plan updates: model-independent, like Thinking (`plan-updates.ts`).
|
|
7705
8069
|
if (planUpdates) {
|