mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
feat: add ultracode session config option (#1438)
Adds an **Ultracode** session config option. It turns on Claude Code's
dynamic workflows for every task, at any effort level. Clients that
support boolean config options get a native switch; the rest get an
On/Off select. This follows the Fast mode pattern.
## How the CLI handles ultracode (2.1.310, checked against the bundled
binary and a live session)
- **Which models:** ultracode works on exactly the models that support
`xhigh` effort. The CLI uses the same check for both. So Opus 4.7+,
Sonnet 5+, Haiku 5.5 and Fable qualify; Opus 4.6 and Haiku 4.5 don't.
Effort level no longer matters.
- **Workflows:** dynamic workflows must be enabled. They are off by
default on Pro plans, and org or managed policy can turn them off.
- **State reporting:** `query.getSettings()` returns
`applied.ultracodeAvailable` and `applied.ultracodeRequested`. The CLI's
schema says hosts should "offer an Ultracode control only while this is
true". This method exists at runtime but is missing from the SDK's
`.d.ts`, so the adapter reads it with a cast and falls back to settings
and model data when it is absent.
- **Unsupported requests:** `applyFlagSettings({ultracode})` is never
validated. The request is stored and stays in place across model
switches; it takes effect whenever the model supports it.
- **Effort changes:** an `effortLevel` change sent without an
`ultracode` key turns ultracode off.
## Behavior
- **Visibility:** at session start, one `getSettings()` call decides
availability and the starting value. Model switches combine the
workflows result learned at start with the new model's
`supportedEffortLevels`, so the switch path makes no extra control
request.
- **Value:** the session tracks the user's choice, like
`fastModeEnabled`. The option hides on unsupported models and comes back
with the same value.
- **Effort applies:** `effortFlagSettings` now carries the session's
toggle instead of `settings.ultracode`. This applies at the initial
apply, on model switch, and from the effort picker, so effort changes no
longer turn ultracode off.
- **Clear context:** the clear-context restart restores the toggle, as
it does Fast mode.
## Known limitation
If a user types `/effort ultracode on` as a prompt, the toggle does not
update. The adapter already doesn't pick up effort changes typed as
`/effort` commands.
## Testing
- New `src/tests/ultracode-config.test.ts`. It covers the helpers and
session behavior: seeding from the CLI's report, hiding while workflows
are off, following the model's support, keeping the value across effort
and model switches, and rejected applies.
- New clear-context restore test. `effortFlagSettings` tests updated.
- Live probe against CLI 2.1.310 confirmed the `getSettings` fields, the
`xhigh` proxy (`claude-opus-4-6` reports unavailable while the request
stays in place), and the effort/key behavior.
- `npm run check`, `npm run build`, `npm run test:run` all pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ This tool implements an ACP agent by using the official [Claude Agent SDK](https
|
||||
- Interactive (and background) terminals
|
||||
- Custom [Slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands)
|
||||
- Client MCP servers
|
||||
- An Ultracode session config option, which turns on Claude Code's dynamic workflows for every task. It appears when dynamic workflows are enabled and the model supports ultracode (any model with `xhigh` effort)
|
||||
- `/mcp` in the chat: the MCP server status as a list. The adapter runs `/mcp reconnect`, `/mcp enable`, and `/mcp disable` through the SDK control API, because Claude Code refuses them in SDK mode. A reconnect of an ACP server that needs authentication starts MCP OAuth through URL elicitation
|
||||
- Session-scoped long-running goals for AIR through the [goal extension](docs/air-extensions.md#goal) under `_meta.jetbrains.air.goal`
|
||||
- Structured errors, recovery, and warnings through the opt-in [session failure extension](docs/air-extensions.md#session-failure)
|
||||
|
||||
@@ -707,7 +707,7 @@ A non-empty plan also gets one clear-context choice with the same priority.
|
||||
After that choice, the adapter stops the old private Claude query and hides that internal rejection.
|
||||
It starts a new private query under the same ACP session.
|
||||
It continues the turn with `Implement the following plan:` and the complete plan.
|
||||
The mode, model, agent, effort, Fast mode, and turn usage stay the same.
|
||||
The mode, model, agent, effort, Fast mode, ultracode, and turn usage stay the same.
|
||||
The SDK cannot apply `allowedPrompts` again, so those hints do not survive the handoff.
|
||||
|
||||
The adapter does not allow a callback only because the session mode is `bypassPermissions`.
|
||||
|
||||
@@ -365,6 +365,15 @@ import {
|
||||
mergeEffortSettings,
|
||||
settingsEffortForModel,
|
||||
} from "./session-effort.js";
|
||||
import {
|
||||
createUltracodeConfigOption,
|
||||
readUltracodeSettings,
|
||||
resolveUltracodeEnabled,
|
||||
ULTRACODE_CONFIG_ID,
|
||||
ultracodeSupported,
|
||||
ultracodeWorkflowsEnabled,
|
||||
type UltracodeOptionState,
|
||||
} from "./session-ultracode.js";
|
||||
|
||||
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
||||
export {
|
||||
@@ -1191,6 +1200,15 @@ export type Session = {
|
||||
* user's intent so it persists across model switches; the Fast mode config
|
||||
* option is only surfaced while the selected model supports it. */
|
||||
fastModeEnabled: boolean;
|
||||
/** Whether ultracode is turned on for this session. Tracked as the user's
|
||||
* intent, like `fastModeEnabled`: the CLI keeps the request across model
|
||||
* switches and applies it whenever the model supports ultracode, while the
|
||||
* config option is only surfaced on supporting models. */
|
||||
ultracodeEnabled: boolean;
|
||||
/** Whether dynamic workflows are enabled, learned from the CLI's
|
||||
* `get_settings` at session start; undefined when unknown. Combined with
|
||||
* the model's support to decide whether to show the ultracode option. */
|
||||
ultracodeWorkflowsEnabled?: boolean;
|
||||
/** The non-default effort the user picked through the ACP picker this
|
||||
* session. A pin lives at the SDK's flag layer, which overrides the CLI's
|
||||
* persisted effort (including the per-model `modelSettings` entries), so it
|
||||
@@ -2801,6 +2819,7 @@ export class ClaudeAcpAgent {
|
||||
return session;
|
||||
},
|
||||
applyFastMode: (session, enabled) => this.applyFastMode(session, enabled),
|
||||
applyUltracode: (session, enabled) => this.applyUltracode(session, enabled),
|
||||
sessionUpdate: (notification) => this.client.sessionUpdate(notification),
|
||||
supportsPlanOperations: () => this.clientCapabilities?.plan != null,
|
||||
ensureConsumer: (session, id) => this.ensureConsumer(session, id),
|
||||
@@ -9203,13 +9222,18 @@ export class ClaudeAcpAgent {
|
||||
throw new Error(`Unknown config option: ${params.configId}`);
|
||||
}
|
||||
|
||||
// Fast mode carries a boolean value (for Clients that opted into boolean
|
||||
// config options) or the "on"/"off" select fallback, so it bypasses the
|
||||
// Fast mode and ultracode carry a boolean value (for Clients that opted
|
||||
// into boolean config options) or the "on"/"off" select fallback, so they
|
||||
// bypass the
|
||||
// string-only validation the select-style options below rely on.
|
||||
if (params.configId === FAST_MODE_CONFIG_ID) {
|
||||
await this.applyFastMode(session, resolveFastModeEnabled(params));
|
||||
return { configOptions: session.configOptions };
|
||||
}
|
||||
if (params.configId === ULTRACODE_CONFIG_ID) {
|
||||
await this.applyUltracode(session, resolveUltracodeEnabled(params));
|
||||
return { configOptions: session.configOptions };
|
||||
}
|
||||
|
||||
if (typeof params.value !== "string") {
|
||||
throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
|
||||
@@ -11038,6 +11062,13 @@ export class ClaudeAcpAgent {
|
||||
{
|
||||
useRecommendedValue,
|
||||
},
|
||||
{
|
||||
// Like Fast mode, the toggle hides on models without ultracode
|
||||
// support and comes back with the retained intent on one that has it.
|
||||
supported: ultracodeSupported(session.ultracodeWorkflowsEnabled, newModelInfo),
|
||||
enabled: session.ultracodeEnabled,
|
||||
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
||||
},
|
||||
);
|
||||
|
||||
// Opted-in clients apply the concrete displayed effort on every switch,
|
||||
@@ -11064,10 +11095,7 @@ export class ClaudeAcpAgent {
|
||||
// deliberately apply their concrete displayed effort.
|
||||
effortFlagSettings(
|
||||
useRecommendedValue ? newEffort : undefined,
|
||||
mergeEffortSettings(
|
||||
session.settingsManager.getSettings(),
|
||||
session.effortSettingsOverride,
|
||||
),
|
||||
session.ultracodeEnabled,
|
||||
),
|
||||
);
|
||||
session.effortPinnedLevel = effortPinnedForNewModel ? pinnedEffort : undefined;
|
||||
@@ -11136,15 +11164,7 @@ export class ClaudeAcpAgent {
|
||||
} else if (configId === EFFORT_CONFIG_ID) {
|
||||
// Apply first so a rejected control request cannot leave the displayed
|
||||
// value ahead of the SDK flag layer.
|
||||
await session.query.applyFlagSettings(
|
||||
effortFlagSettings(
|
||||
value,
|
||||
mergeEffortSettings(
|
||||
session.settingsManager.getSettings(),
|
||||
session.effortSettingsOverride,
|
||||
),
|
||||
),
|
||||
);
|
||||
await session.query.applyFlagSettings(effortFlagSettings(value, session.ultracodeEnabled));
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
||||
);
|
||||
@@ -11226,6 +11246,22 @@ export class ClaudeAcpAgent {
|
||||
this.refreshFastModeOption(session, enabled);
|
||||
}
|
||||
|
||||
/** Turn ultracode on or off for a session. Sends an explicit boolean, which
|
||||
* leaves the effort level alone (`null` would reset the flag layer instead).
|
||||
* The SDK flag is applied first so a rejected request leaves the session
|
||||
* state and option untouched. */
|
||||
private async applyUltracode(session: Session, enabled: boolean): Promise<void> {
|
||||
await session.query.applyFlagSettings({ ultracode: enabled });
|
||||
session.ultracodeEnabled = enabled;
|
||||
const refreshed = createUltracodeConfigOption(
|
||||
enabled,
|
||||
clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
||||
);
|
||||
session.configOptions = session.configOptions.map((o) =>
|
||||
o.id === ULTRACODE_CONFIG_ID ? refreshed : o,
|
||||
);
|
||||
}
|
||||
|
||||
/** Reconcile the session's Fast mode toggle with an SDK-reported
|
||||
* `fast_mode_state` (delivered on `system`/init and on user-turn `result`s).
|
||||
* The SDK can flip fast mode independently of the user — e.g. back to `on`
|
||||
@@ -12064,6 +12100,21 @@ export class ClaudeAcpAgent {
|
||||
settingsManager.getSettings(),
|
||||
configuredSettingsObject,
|
||||
);
|
||||
|
||||
// The CLI reports whether ultracode can run (workflows enabled and a
|
||||
// supporting model) and whether the session already requests it (an
|
||||
// `ultracode: true` setting). Without a report, fall back to settings.
|
||||
const ultracodeReport = await readUltracodeSettings(q);
|
||||
timing.phase("ultracode");
|
||||
const ultracodeEnabled = ultracodeReport?.requested ?? effortSettings.ultracode === true;
|
||||
const workflowsEnabled = ultracodeWorkflowsEnabled(ultracodeReport, currentModelInfo);
|
||||
const ultracode: UltracodeOptionState = {
|
||||
supported:
|
||||
ultracodeReport?.available ?? ultracodeSupported(workflowsEnabled, currentModelInfo),
|
||||
enabled: ultracodeEnabled,
|
||||
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
||||
};
|
||||
|
||||
const configOptions = buildConfigOptions(
|
||||
modes,
|
||||
models,
|
||||
@@ -12075,10 +12126,11 @@ export class ClaudeAcpAgent {
|
||||
{
|
||||
useRecommendedValue,
|
||||
},
|
||||
ultracode,
|
||||
);
|
||||
const initialEffort = configOptions.find((option) => option.id === EFFORT_CONFIG_ID);
|
||||
if (useRecommendedValue && typeof initialEffort?.currentValue === "string") {
|
||||
await q.applyFlagSettings(effortFlagSettings(initialEffort.currentValue, effortSettings));
|
||||
await q.applyFlagSettings(effortFlagSettings(initialEffort.currentValue, ultracodeEnabled));
|
||||
}
|
||||
// Seed the context window without awaited IPC. The cached authoritative
|
||||
// window from a prior turn wins (`result.modelUsage`, cross-session),
|
||||
@@ -12146,6 +12198,8 @@ export class ClaudeAcpAgent {
|
||||
currentAgent,
|
||||
fastModeEnabled,
|
||||
fastModeDisabledReason,
|
||||
ultracodeEnabled,
|
||||
ultracodeWorkflowsEnabled: workflowsEnabled,
|
||||
abortController,
|
||||
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
|
||||
forwardSubagentText,
|
||||
@@ -12975,6 +13029,7 @@ export function buildConfigOptions(
|
||||
currentAgent: string = DEFAULT_AGENT_ID,
|
||||
fastMode?: FastModeOptionState,
|
||||
presentation?: ConfigOptionPresentation,
|
||||
ultracode?: UltracodeOptionState,
|
||||
): SessionConfigOption[] {
|
||||
const options: SessionConfigOption[] = [SessionModeManager.configOption(modes)];
|
||||
// A gateway that lists none of the SDK's models leaves no model to offer, so
|
||||
@@ -13005,6 +13060,12 @@ export function buildConfigOptions(
|
||||
);
|
||||
}
|
||||
|
||||
// Surface the ultracode toggle only while the CLI can run it: dynamic
|
||||
// workflows are enabled and the current model supports it.
|
||||
if (ultracode?.supported) {
|
||||
options.push(createUltracodeConfigOption(ultracode.enabled, ultracode.useBooleanOption));
|
||||
}
|
||||
|
||||
// Only surface the Agent picker when there's a real choice — i.e. the user
|
||||
// has configured at least one custom agent (built-ins are filtered out in
|
||||
// discoverCustomAgents). With none configured, "Default" would be the only
|
||||
|
||||
@@ -39,6 +39,7 @@ export type ClearContextSession<Turn extends ClearContextTurn = ClearContextTurn
|
||||
configOptions: SessionConfigOption[];
|
||||
currentAgent: string;
|
||||
fastModeEnabled: boolean;
|
||||
ultracodeEnabled: boolean;
|
||||
effortPinnedLevel?: string;
|
||||
activeTurn?: Turn | null;
|
||||
turnQueue?: Turn[];
|
||||
@@ -59,6 +60,7 @@ export type ClearContextCoordinatorHost<
|
||||
options: { publicSessionId: string; permissionMode: PermissionMode },
|
||||
): Promise<Session>;
|
||||
applyFastMode(session: Session, enabled: boolean): Promise<void>;
|
||||
applyUltracode(session: Session, enabled: boolean): Promise<void>;
|
||||
publishSessionState(
|
||||
sessionId: string,
|
||||
mode: PermissionMode,
|
||||
@@ -146,6 +148,16 @@ export async function continuePlanInFreshContext<
|
||||
}
|
||||
assertRestartActive(freshSession);
|
||||
}
|
||||
// The fresh query seeds ultracode from settings only, so a toggle made
|
||||
// during the old session would otherwise be lost.
|
||||
if (oldSession.ultracodeEnabled !== freshSession.ultracodeEnabled) {
|
||||
try {
|
||||
await host.applyUltracode(freshSession, oldSession.ultracodeEnabled);
|
||||
} catch (error) {
|
||||
host.logError("Failed to restore ultracode after clearing context:", error);
|
||||
}
|
||||
assertRestartActive(freshSession);
|
||||
}
|
||||
|
||||
oldSession.activeTurn = null;
|
||||
oldSession.turnQueue = [];
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
export const DEFAULT_MODEL_ID = "default";
|
||||
export const DEFAULT_AGENT_ID = "default";
|
||||
export const EFFORT_CONFIG_ID = "effort";
|
||||
export const ULTRACODE_CONFIG_ID = "ultracode";
|
||||
|
||||
@@ -14,14 +14,15 @@ export function toSdkEffortLevel(value: string | undefined): EffortLevel | null
|
||||
|
||||
// Since CLI 2.1.284 an `effortLevel` that changes the level, sent without an
|
||||
// `ultracode` key, turns ultracode off. The CLI's own effort control keeps
|
||||
// ultracode on at any level, so carry a settings-requested ultracode along
|
||||
// with every effort apply.
|
||||
// ultracode on at any level, so send `ultracode: true` with every effort apply
|
||||
// while the session has ultracode on. When it is off, the key is omitted: an
|
||||
// effort change then leaves ultracode off, as it already was.
|
||||
export function effortFlagSettings(
|
||||
value: string | undefined,
|
||||
settings: Settings,
|
||||
ultracode: boolean,
|
||||
): { effortLevel: EffortLevel | null; ultracode?: true } {
|
||||
const effortLevel = toSdkEffortLevel(value);
|
||||
return settings.ultracode === true ? { effortLevel, ultracode: true } : { effortLevel };
|
||||
return ultracode ? { effortLevel, ultracode: true } : { effortLevel };
|
||||
}
|
||||
|
||||
function canonicalizeModelSettingsKey(value: string): string {
|
||||
|
||||
136
src/session-ultracode.ts
Normal file
136
src/session-ultracode.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type {
|
||||
SessionConfigOption,
|
||||
SetSessionConfigOptionRequest,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import type { ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { ULTRACODE_CONFIG_ID } from "./session-config-ids.js";
|
||||
|
||||
export { ULTRACODE_CONFIG_ID } from "./session-config-ids.js";
|
||||
|
||||
/** Select-fallback values for clients without boolean config options. */
|
||||
export const ULTRACODE_ON = "on";
|
||||
export const ULTRACODE_OFF = "off";
|
||||
const ULTRACODE_DESCRIPTION = "Dynamic workflows on every task, at any effort level";
|
||||
|
||||
/** Ultracode state from the CLI's `get_settings` control response
|
||||
* (`applied.ultracodeAvailable` / `applied.ultracodeRequested`). Each field is
|
||||
* undefined when the CLI does not report it. */
|
||||
export type UltracodeSettingsReport = {
|
||||
/** Workflows are enabled and the current model supports ultracode. */
|
||||
available?: boolean;
|
||||
/** Ultracode is turned on for the session, whether or not it can take
|
||||
* effect on the current model. */
|
||||
requested?: boolean;
|
||||
};
|
||||
|
||||
// `Query.getSettings()` exists at runtime (sdk.mjs) but is missing from the
|
||||
// SDK's type declarations as of 0.3.310.
|
||||
type GetSettingsQuery = Query & {
|
||||
getSettings?: () => Promise<{
|
||||
applied?: { ultracodeAvailable?: unknown; ultracodeRequested?: unknown };
|
||||
}>;
|
||||
};
|
||||
|
||||
/** Ask the CLI whether ultracode is available and requested for the session.
|
||||
* Returns undefined when the SDK has no `getSettings` or the request fails;
|
||||
* callers then fall back to {@link modelSupportsUltracode}. */
|
||||
export async function readUltracodeSettings(
|
||||
query: Query,
|
||||
): Promise<UltracodeSettingsReport | undefined> {
|
||||
const getSettings = (query as GetSettingsQuery).getSettings;
|
||||
if (typeof getSettings !== "function") return undefined;
|
||||
try {
|
||||
const applied = (await getSettings.call(query))?.applied;
|
||||
return {
|
||||
available:
|
||||
typeof applied?.ultracodeAvailable === "boolean" ? applied.ultracodeAvailable : undefined,
|
||||
requested:
|
||||
typeof applied?.ultracodeRequested === "boolean" ? applied.ultracodeRequested : undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** The CLI supports ultracode on exactly the models that support `xhigh`
|
||||
* effort (both use the same capability check), and `supportedEffortLevels`
|
||||
* lists `xhigh` only for those models. E.g. Opus 4.7+, Sonnet 5+ and Haiku
|
||||
* 5.5 qualify; Opus 4.6 and Haiku 4.5 do not. */
|
||||
export function modelSupportsUltracode(modelInfo: ModelInfo | undefined): boolean {
|
||||
return (
|
||||
modelInfo?.supportsEffort === true &&
|
||||
modelInfo.supportedEffortLevels?.includes("xhigh") === true
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether dynamic workflows are enabled for the session, inferred from the
|
||||
* CLI's `ultracodeAvailable` for the model the session started on. This is
|
||||
* the part of availability that does not change with the model: the plan
|
||||
* default (off on Pro), `enableWorkflows`/`disableWorkflows` and org policy.
|
||||
* Undefined when the report can't tell, e.g. `ultracodeAvailable: false` on a
|
||||
* model that lacks ultracode support anyway. */
|
||||
export function ultracodeWorkflowsEnabled(
|
||||
report: UltracodeSettingsReport | undefined,
|
||||
modelInfo: ModelInfo | undefined,
|
||||
): boolean | undefined {
|
||||
if (report?.available === true) return true;
|
||||
if (report?.available === false && modelSupportsUltracode(modelInfo)) return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Whether to offer the ultracode toggle for `modelInfo`. An unknown workflows
|
||||
* state shows the toggle: the CLI keeps an unavailable request without
|
||||
* error, so the toggle is merely inert there. */
|
||||
export function ultracodeSupported(
|
||||
workflowsEnabled: boolean | undefined,
|
||||
modelInfo: ModelInfo | undefined,
|
||||
): boolean {
|
||||
return workflowsEnabled !== false && modelSupportsUltracode(modelInfo);
|
||||
}
|
||||
|
||||
/** Ultracode state threaded into `buildConfigOptions`. The option is only
|
||||
* surfaced while `supported`. */
|
||||
export type UltracodeOptionState = {
|
||||
supported: boolean;
|
||||
enabled: boolean;
|
||||
/** Whether the client opted into boolean config options. */
|
||||
useBooleanOption: boolean;
|
||||
};
|
||||
|
||||
/** Build the ultracode toggle: a native boolean for clients that opted into
|
||||
* boolean config options, otherwise an "on"/"off" select. */
|
||||
export function createUltracodeConfigOption(
|
||||
enabled: boolean,
|
||||
useBooleanOption: boolean,
|
||||
): SessionConfigOption {
|
||||
const base = {
|
||||
id: ULTRACODE_CONFIG_ID,
|
||||
name: "Ultracode",
|
||||
description: ULTRACODE_DESCRIPTION,
|
||||
category: "model_config",
|
||||
} as const;
|
||||
|
||||
if (useBooleanOption) {
|
||||
return { ...base, type: "boolean", currentValue: enabled };
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
type: "select",
|
||||
currentValue: enabled ? ULTRACODE_ON : ULTRACODE_OFF,
|
||||
options: [
|
||||
{ value: ULTRACODE_ON, name: "On" },
|
||||
{ value: ULTRACODE_OFF, name: "Off" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** Read the requested ultracode value from a `session/set_config_option`
|
||||
* request: a boolean, or the "on"/"off" select-fallback strings. */
|
||||
export function resolveUltracodeEnabled(params: SetSessionConfigOptionRequest): boolean {
|
||||
const value = params.value;
|
||||
if (typeof value === "boolean") return value;
|
||||
if (value === ULTRACODE_ON) return true;
|
||||
if (value === ULTRACODE_OFF) return false;
|
||||
throw new Error(`Invalid value for config option ${ULTRACODE_CONFIG_ID}: ${value}`);
|
||||
}
|
||||
@@ -13978,6 +13978,7 @@ describe("session/close", () => {
|
||||
agents: [],
|
||||
currentAgent: "default",
|
||||
fastModeEnabled: false,
|
||||
ultracodeEnabled: false,
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
forwardSubagentText: false,
|
||||
@@ -14099,6 +14100,7 @@ describe("session/delete", () => {
|
||||
agents: [],
|
||||
currentAgent: "default",
|
||||
fastModeEnabled: false,
|
||||
ultracodeEnabled: false,
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
forwardSubagentText: false,
|
||||
@@ -14258,6 +14260,7 @@ describe("getOrCreateSession param change detection", () => {
|
||||
agents: [],
|
||||
currentAgent: "default",
|
||||
fastModeEnabled: false,
|
||||
ultracodeEnabled: false,
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
forwardSubagentText: false,
|
||||
@@ -18712,6 +18715,7 @@ describe("post-error recovery", () => {
|
||||
agents: [],
|
||||
currentAgent: "default",
|
||||
fastModeEnabled: false,
|
||||
ultracodeEnabled: false,
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
forwardSubagentText: false,
|
||||
@@ -27068,6 +27072,7 @@ describe("session/cancel wedge recovery (issue #680)", () => {
|
||||
agents: [],
|
||||
currentAgent: "default",
|
||||
fastModeEnabled: false,
|
||||
ultracodeEnabled: false,
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
forwardSubagentText: false,
|
||||
@@ -28975,6 +28980,7 @@ describe("agent selection config option", () => {
|
||||
agents,
|
||||
currentAgent: "default",
|
||||
fastModeEnabled: false,
|
||||
ultracodeEnabled: false,
|
||||
abortController: new AbortController(),
|
||||
emitRawSDKMessages: false,
|
||||
forwardSubagentText: false,
|
||||
|
||||
@@ -31,6 +31,7 @@ function testSession(overrides: Partial<TestSession> = {}): TestSession {
|
||||
configOptions: [],
|
||||
currentAgent: "default",
|
||||
fastModeEnabled: false,
|
||||
ultracodeEnabled: false,
|
||||
input: { push: vi.fn() },
|
||||
...overrides,
|
||||
};
|
||||
@@ -49,6 +50,7 @@ function testHost(
|
||||
return freshSession;
|
||||
}),
|
||||
applyFastMode: vi.fn(async () => {}),
|
||||
applyUltracode: vi.fn(async () => {}),
|
||||
publishSessionState: vi.fn(async () => {}),
|
||||
continuationMessage: vi.fn((sessionId, plan, promptUuid) => ({
|
||||
type: "user" as const,
|
||||
@@ -252,6 +254,25 @@ describe("continuePlanInFreshContext", () => {
|
||||
expect(host.applyFastMode).toHaveBeenCalledWith(freshSession, false);
|
||||
});
|
||||
|
||||
it("restores ultracode turned on before clearing context", async () => {
|
||||
const turn: ClearContextTurn = {
|
||||
settled: false,
|
||||
promptUuid: "00000000-0000-4000-8000-000000000000",
|
||||
};
|
||||
const oldSession = testSession({ activeTurn: turn, ultracodeEnabled: true });
|
||||
const freshSession = testSession({ ultracodeEnabled: false });
|
||||
const host = testHost(oldSession, freshSession);
|
||||
|
||||
await continuePlanInFreshContext(
|
||||
"public-session",
|
||||
oldSession,
|
||||
{ toolUseId: "tool-plan", plan: "Ship it", mode: "auto" },
|
||||
host,
|
||||
);
|
||||
|
||||
expect(host.applyUltracode).toHaveBeenCalledWith(freshSession, true);
|
||||
});
|
||||
|
||||
it("does not publish or continue a restart cancelled while creating the fresh session", async () => {
|
||||
const turn: ClearContextTurn = {
|
||||
settled: false,
|
||||
|
||||
@@ -31,6 +31,9 @@ export function makeMockQuery(overrides: Record<string, unknown> = {}) {
|
||||
supportedCommands: async () => [],
|
||||
mcpServerStatus: async () => [],
|
||||
getContextUsage: async () => DEFAULT_CONTEXT_USAGE,
|
||||
// No ultracode fields: session creation falls back to settings and the
|
||||
// model's effort levels, as with a CLI that does not report them.
|
||||
getSettings: async () => ({ applied: {} }),
|
||||
close: () => {},
|
||||
interrupt: async () => undefined,
|
||||
[Symbol.asyncIterator]: async function* () {},
|
||||
|
||||
@@ -381,17 +381,17 @@ describe("settingsEffortForModel", () => {
|
||||
});
|
||||
|
||||
describe("effortFlagSettings", () => {
|
||||
it("carries ultracode only when settings request it", () => {
|
||||
expect(effortFlagSettings("high", { ultracode: true })).toEqual({
|
||||
it("carries ultracode only while the session has it on", () => {
|
||||
expect(effortFlagSettings("high", true)).toEqual({
|
||||
effortLevel: "high",
|
||||
ultracode: true,
|
||||
});
|
||||
expect(effortFlagSettings("default", { ultracode: true })).toEqual({
|
||||
expect(effortFlagSettings("default", true)).toEqual({
|
||||
effortLevel: null,
|
||||
ultracode: true,
|
||||
});
|
||||
expect(effortFlagSettings("high", { ultracode: false })).toEqual({ effortLevel: "high" });
|
||||
expect(effortFlagSettings(undefined, {})).toEqual({ effortLevel: null });
|
||||
expect(effortFlagSettings("high", false)).toEqual({ effortLevel: "high" });
|
||||
expect(effortFlagSettings(undefined, false)).toEqual({ effortLevel: null });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
292
src/tests/ultracode-config.test.ts
Normal file
292
src/tests/ultracode-config.test.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { AcpClient, ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||
import {
|
||||
createUltracodeConfigOption,
|
||||
modelSupportsUltracode,
|
||||
readUltracodeSettings,
|
||||
resolveUltracodeEnabled,
|
||||
ULTRACODE_CONFIG_ID,
|
||||
ULTRACODE_OFF,
|
||||
ULTRACODE_ON,
|
||||
ultracodeSupported,
|
||||
ultracodeWorkflowsEnabled,
|
||||
} from "../session-ultracode.js";
|
||||
import { makeMockQuery } from "./helpers.js";
|
||||
|
||||
const { querySpy } = vi.hoisted(() => ({ querySpy: vi.fn() }));
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
const actual = await vi.importActual<any>("@anthropic-ai/claude-agent-sdk");
|
||||
return { ...actual, query: querySpy };
|
||||
});
|
||||
|
||||
const OPUS: ModelInfo = {
|
||||
value: "opus",
|
||||
displayName: "Opus",
|
||||
description: "",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high", "xhigh", "max"],
|
||||
};
|
||||
const FABLE: ModelInfo = {
|
||||
value: "fable",
|
||||
displayName: "Fable",
|
||||
description: "",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high", "xhigh", "max"],
|
||||
};
|
||||
// Opus 4.6 and Haiku 4.5 have no `xhigh` effort, so the CLI has no ultracode
|
||||
// for them either.
|
||||
const OLD_OPUS: ModelInfo = {
|
||||
value: "claude-opus-4-6",
|
||||
displayName: "Opus 4.6",
|
||||
description: "",
|
||||
supportsEffort: true,
|
||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
||||
};
|
||||
|
||||
function getSettingsQuery(getSettings: unknown): Query {
|
||||
return { getSettings } as unknown as Query;
|
||||
}
|
||||
|
||||
describe("ultracode helpers", () => {
|
||||
it("builds a boolean option or an on/off select", () => {
|
||||
expect(createUltracodeConfigOption(true, true)).toEqual({
|
||||
id: ULTRACODE_CONFIG_ID,
|
||||
name: "Ultracode",
|
||||
description: "Dynamic workflows on every task, at any effort level",
|
||||
category: "model_config",
|
||||
type: "boolean",
|
||||
currentValue: true,
|
||||
});
|
||||
expect(createUltracodeConfigOption(false, false)).toMatchObject({
|
||||
type: "select",
|
||||
currentValue: ULTRACODE_OFF,
|
||||
options: [
|
||||
{ value: ULTRACODE_ON, name: "On" },
|
||||
{ value: ULTRACODE_OFF, name: "Off" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts booleans and the select strings", () => {
|
||||
const request = (value: unknown) =>
|
||||
({ sessionId: "s", configId: ULTRACODE_CONFIG_ID, value }) as never;
|
||||
expect(resolveUltracodeEnabled(request(true))).toBe(true);
|
||||
expect(resolveUltracodeEnabled(request(ULTRACODE_OFF))).toBe(false);
|
||||
expect(() => resolveUltracodeEnabled(request("maybe"))).toThrow(
|
||||
"Invalid value for config option ultracode: maybe",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats a model as supporting ultracode when it supports xhigh effort", () => {
|
||||
expect(modelSupportsUltracode(OPUS)).toBe(true);
|
||||
expect(modelSupportsUltracode(OLD_OPUS)).toBe(false);
|
||||
expect(modelSupportsUltracode({ ...OPUS, supportsEffort: false })).toBe(false);
|
||||
expect(modelSupportsUltracode(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("infers the workflows gate only when the start model can tell", () => {
|
||||
expect(ultracodeWorkflowsEnabled({ available: true }, OLD_OPUS)).toBe(true);
|
||||
expect(ultracodeWorkflowsEnabled({ available: false }, OPUS)).toBe(false);
|
||||
// Opus 4.6 is unavailable regardless of workflows.
|
||||
expect(ultracodeWorkflowsEnabled({ available: false }, OLD_OPUS)).toBeUndefined();
|
||||
expect(ultracodeWorkflowsEnabled(undefined, OPUS)).toBeUndefined();
|
||||
|
||||
expect(ultracodeSupported(undefined, OPUS)).toBe(true);
|
||||
expect(ultracodeSupported(false, OPUS)).toBe(false);
|
||||
expect(ultracodeSupported(true, OLD_OPUS)).toBe(false);
|
||||
});
|
||||
|
||||
it("reads the CLI's ultracode report and tolerates CLIs without one", async () => {
|
||||
await expect(
|
||||
readUltracodeSettings(
|
||||
getSettingsQuery(async () => ({
|
||||
applied: { ultracodeAvailable: true, ultracodeRequested: false },
|
||||
})),
|
||||
),
|
||||
).resolves.toEqual({ available: true, requested: false });
|
||||
await expect(
|
||||
readUltracodeSettings(getSettingsQuery(async () => ({ applied: {} }))),
|
||||
).resolves.toEqual({ available: undefined, requested: undefined });
|
||||
await expect(readUltracodeSettings(getSettingsQuery(undefined))).resolves.toBeUndefined();
|
||||
await expect(
|
||||
readUltracodeSettings(
|
||||
getSettingsQuery(async () => {
|
||||
throw new Error("unsupported control request");
|
||||
}),
|
||||
),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ultracode session config option", () => {
|
||||
let tempDir: string;
|
||||
let originalClaudeConfigDir: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "acp-ultracode-"));
|
||||
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
process.env.CLAUDE_CONFIG_DIR = tempDir;
|
||||
querySpy.mockReset();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (originalClaudeConfigDir) {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
|
||||
} else {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
}
|
||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function createSession(opts: {
|
||||
models?: ModelInfo[];
|
||||
getSettings?: () => Promise<unknown>;
|
||||
settings?: Record<string, unknown>;
|
||||
clientCapabilities?: ClientCapabilities;
|
||||
}) {
|
||||
if (opts.settings) {
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "settings.json"),
|
||||
JSON.stringify(opts.settings),
|
||||
);
|
||||
}
|
||||
const applyFlagSettings = vi.fn();
|
||||
querySpy.mockReturnValue(
|
||||
makeMockQuery({
|
||||
initializationResult: async () => ({ models: opts.models ?? [OPUS, FABLE, OLD_OPUS] }),
|
||||
applyFlagSettings,
|
||||
...(opts.getSettings ? { getSettings: opts.getSettings } : {}),
|
||||
}),
|
||||
);
|
||||
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent({
|
||||
sessionUpdate: async () => {},
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
} as unknown as AcpClient);
|
||||
(agent as any).clientCapabilities = opts.clientCapabilities ?? {
|
||||
session: { configOptions: { boolean: {} } },
|
||||
};
|
||||
const response = await (agent as any).createSession({
|
||||
cwd: tempDir,
|
||||
mcpServers: [],
|
||||
_meta: { disableBuiltInTools: true },
|
||||
});
|
||||
const sessionId: string = response.sessionId;
|
||||
const ultracodeOption = () =>
|
||||
agent.sessions[sessionId].configOptions.find((o) => o.id === ULTRACODE_CONFIG_ID);
|
||||
return { agent, sessionId, applyFlagSettings, ultracodeOption };
|
||||
}
|
||||
|
||||
it("seeds the option from the CLI's report", async () => {
|
||||
const { ultracodeOption, agent, sessionId } = await createSession({
|
||||
getSettings: async () => ({
|
||||
applied: { ultracodeAvailable: true, ultracodeRequested: true },
|
||||
}),
|
||||
});
|
||||
expect(ultracodeOption()).toEqual(createUltracodeConfigOption(true, true));
|
||||
expect(agent.sessions[sessionId].ultracodeEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("hides the option while dynamic workflows are off, across model switches", async () => {
|
||||
const { agent, sessionId, ultracodeOption } = await createSession({
|
||||
getSettings: async () => ({
|
||||
applied: { ultracodeAvailable: false, ultracodeRequested: false },
|
||||
}),
|
||||
});
|
||||
expect(ultracodeOption()).toBeUndefined();
|
||||
|
||||
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "fable" });
|
||||
expect(ultracodeOption()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("follows the model's support and keeps the toggle across switches", async () => {
|
||||
const { agent, sessionId, applyFlagSettings, ultracodeOption } = await createSession({
|
||||
clientCapabilities: {},
|
||||
});
|
||||
expect(ultracodeOption()).toEqual(createUltracodeConfigOption(false, false));
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId,
|
||||
configId: ULTRACODE_CONFIG_ID,
|
||||
value: ULTRACODE_ON,
|
||||
});
|
||||
expect(applyFlagSettings).toHaveBeenLastCalledWith({ ultracode: true });
|
||||
|
||||
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "claude-opus-4-6" });
|
||||
expect(ultracodeOption()).toBeUndefined();
|
||||
|
||||
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "fable" });
|
||||
expect(ultracodeOption()).toEqual(createUltracodeConfigOption(true, false));
|
||||
});
|
||||
|
||||
it("keeps a toggled-on ultracode when effort changes, and not a toggled-off one", async () => {
|
||||
// An effortLevel apply without an ultracode key turns ultracode off, so
|
||||
// the effort picker must carry the session's toggle.
|
||||
const { agent, sessionId, applyFlagSettings } = await createSession({
|
||||
settings: { ultracode: true },
|
||||
});
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId,
|
||||
configId: ULTRACODE_CONFIG_ID,
|
||||
type: "boolean",
|
||||
value: false,
|
||||
});
|
||||
expect(applyFlagSettings).toHaveBeenLastCalledWith({ ultracode: false });
|
||||
await agent.setSessionConfigOption({ sessionId, configId: "effort", value: "high" });
|
||||
expect(applyFlagSettings).toHaveBeenLastCalledWith({ effortLevel: "high" });
|
||||
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId,
|
||||
configId: ULTRACODE_CONFIG_ID,
|
||||
type: "boolean",
|
||||
value: true,
|
||||
});
|
||||
await agent.setSessionConfigOption({ sessionId, configId: "effort", value: "low" });
|
||||
expect(applyFlagSettings).toHaveBeenLastCalledWith({ effortLevel: "low", ultracode: true });
|
||||
});
|
||||
|
||||
it("keeps ultracode on through the effort apply that follows a model switch", async () => {
|
||||
// recommendedValue clients apply the new model's effort on every switch.
|
||||
const { agent, sessionId, applyFlagSettings } = await createSession({
|
||||
clientCapabilities: {
|
||||
session: { configOptions: { boolean: {} } },
|
||||
_meta: { jetbrains: { air: { version: 1, capabilities: ["recommendedValue"] } } },
|
||||
},
|
||||
});
|
||||
await agent.setSessionConfigOption({
|
||||
sessionId,
|
||||
configId: ULTRACODE_CONFIG_ID,
|
||||
type: "boolean",
|
||||
value: true,
|
||||
});
|
||||
|
||||
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "fable" });
|
||||
expect(applyFlagSettings).toHaveBeenLastCalledWith({ effortLevel: "medium", ultracode: true });
|
||||
});
|
||||
|
||||
it("does not change session state when the SDK rejects the flag", async () => {
|
||||
const { agent, sessionId, applyFlagSettings, ultracodeOption } = await createSession({});
|
||||
applyFlagSettings.mockRejectedValueOnce(new Error("nope"));
|
||||
|
||||
await expect(
|
||||
agent.setSessionConfigOption({
|
||||
sessionId,
|
||||
configId: ULTRACODE_CONFIG_ID,
|
||||
type: "boolean",
|
||||
value: true,
|
||||
}),
|
||||
).rejects.toThrow("nope");
|
||||
expect(agent.sessions[sessionId].ultracodeEnabled).toBe(false);
|
||||
expect(ultracodeOption()).toEqual(createUltracodeConfigOption(false, true));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user