Skip to content
1 change: 1 addition & 0 deletions docs/users/integration-jetbrains.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- **Agent Client Protocol**: Full support for ACP enabling advanced IDE interactions
- **Symbol management**: #-mention files to add them to the conversation context
- **Conversation history**: Access to past conversations within the IDE
- **Reasoning effort**: Choose Default, Low, Medium, High, Extra high, or Max from the agent's session options; each provider maps or clamps the requested tier for the active model
- **Context usage**: See the current context-window occupancy while Qwen Code works

### Requirements
Expand Down
70 changes: 69 additions & 1 deletion integration-tests/cli/acp-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ function setupAcpTest(
}
});

it('supports session/set_config_option for mode and model', async () => {
it('supports session/set_config_option for mode, model, and reasoning effort', async () => {
const rig = new TestRig();
// Inject a deterministic openai provider model so `availableModels` always
// contains a settable openai entry. The previous version relied on the
Expand Down Expand Up @@ -533,9 +533,26 @@ function setupAcpTest(
models: {
availableModels: Array<{ modelId: string }>;
};
configOptions: Array<{
id: string;
category?: string;
currentValue: string;
options: Array<{ value: string; name: string }>;
}>;
};
expect(newSession.sessionId).toBeTruthy();

const initialReasoningOption = newSession.configOptions.find(
(opt) => opt.id === 'reasoning_effort',
);
expect(initialReasoningOption).toMatchObject({
category: 'thought_level',
currentValue: 'default',
});
expect(
initialReasoningOption?.options.map((option) => option.value),
).toEqual(['default', 'low', 'medium', 'high', 'xhigh', 'max']);

// Test: Set mode using set_config_option
const setModeResult = (await sendRequest('session/set_config_option', {
sessionId: newSession.sessionId,
Expand Down Expand Up @@ -598,6 +615,57 @@ function setupAcpTest(
);
expect(updatedModelOption).toBeDefined();
expect(updatedModelOption!.currentValue).toBe(openaiModel!.modelId);
expect(
setModelResult.configOptions.find(
(opt) => opt.id === 'reasoning_effort',
)?.currentValue,
).toBe('default');

const setReasoningResult = (await sendRequest(
'session/set_config_option',
{
sessionId: newSession.sessionId,
configId: 'reasoning_effort',
value: 'xhigh',
},
)) as {
configOptions: Array<{ id: string; currentValue: string }>;
};
expect(
setReasoningResult.configOptions.find(
(opt) => opt.id === 'reasoning_effort',
)?.currentValue,
).toBe('xhigh');

const resetReasoningResult = (await sendRequest(
'session/set_config_option',
{
sessionId: newSession.sessionId,
configId: 'reasoning_effort',
value: 'default',
},
)) as {
configOptions: Array<{ id: string; currentValue: string }>;
};
expect(
resetReasoningResult.configOptions.find(
(opt) => opt.id === 'reasoning_effort',
)?.currentValue,
).toBe('default');

await expect(
sendRequest('session/set_config_option', {
sessionId: newSession.sessionId,
configId: 'reasoning_effort',
value: 'ultra',
}),
).rejects.toMatchObject({
response: {
code: -32602,
message:
'Invalid params: Unknown reasoning effort: ultra. Choose one of: default, low, medium, high, xhigh, max',
},
});
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
Expand Down
115 changes: 115 additions & 0 deletions packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,17 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({
})),
APPROVAL_MODE_INFO: {},
APPROVAL_MODES: [],
applyReasoningEffort: (
config: {
setReasoningEffort(effort: string | undefined): void;
getReasoningEffort(): string | undefined;
},
effort: string | undefined,
) => {
config.setReasoningEffort(effort);
return config.getReasoningEffort() === effort;
},
REASONING_EFFORT_TIERS: ['low', 'medium', 'high', 'xhigh', 'max'],
Comment thread
zjunothing marked this conversation as resolved.
ApprovalMode: { YOLO: 'yolo' },
isGatedMcpScope: (scope: unknown) =>
scope === 'project' || scope === 'workspace',
Expand Down Expand Up @@ -1854,6 +1865,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
type AgentLike = {
initialize: (args: Record<string, unknown>) => Promise<unknown>;
newSession: (args: Record<string, unknown>) => Promise<unknown>;
setSessionConfigOption: (args: Record<string, unknown>) => Promise<unknown>;
beginManagedShutdown: () => {
configs: Config[];
writerShutdown: Promise<void>;
Expand Down Expand Up @@ -3198,6 +3210,8 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
getAvailableModels: vi.fn().mockReturnValue([]),
getModes: vi.fn().mockReturnValue([]),
getApprovalMode: vi.fn().mockReturnValue('default'),
getReasoningEffort: vi.fn().mockReturnValue(undefined),
setReasoningEffort: vi.fn(),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getAuthType: vi.fn().mockReturnValue('api-key'),
getAllConfiguredModels: vi.fn().mockReturnValue([]),
Expand Down Expand Up @@ -6212,6 +6226,107 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
await agentPromise;
});

it('exposes and applies the ACP reasoning effort selector', async () => {
const sessionId = 'reasoning-effort-session';
const innerConfig = await setupSessionMocks(sessionId);
let currentEffort: string | undefined;
innerConfig.getReasoningEffort = vi.fn(() => currentEffort);
innerConfig.setReasoningEffort = vi.fn((effort: string | undefined) => {
currentEffort = effort;
});

const { agent, agentPromise } = await bootAcpAgent();
try {
const session = (await agent.newSession({
cwd: '/tmp',
mcpServers: [],
})) as {
configOptions: Array<{
id: string;
category?: string;
currentValue: string;
options: Array<{
value: string;
name: string;
description: string;
}>;
}>;
};

expect(
session.configOptions.find(
(option) => option.id === 'reasoning_effort',
),
).toMatchObject({
category: 'thought_level',
currentValue: 'default',
options: [
{ value: 'default', name: 'Default' },
{ value: 'low', name: 'Low', description: expect.any(String) },
{ value: 'medium', name: 'Medium', description: expect.any(String) },
{ value: 'high', name: 'High', description: expect.any(String) },
{
value: 'xhigh',
name: 'Extra high',
description: expect.any(String),
},
{ value: 'max', name: 'Max', description: expect.any(String) },
],
});

const selected = (await agent.setSessionConfigOption({
sessionId,
configId: 'reasoning_effort',
value: 'xhigh',
})) as { configOptions: typeof session.configOptions };
expect(innerConfig.setReasoningEffort).toHaveBeenCalledWith('xhigh');
expect(
selected.configOptions.find(
(option) => option.id === 'reasoning_effort',
)?.currentValue,
).toBe('xhigh');

const reset = (await agent.setSessionConfigOption({
sessionId,
configId: 'reasoning_effort',
value: 'default',
})) as { configOptions: typeof session.configOptions };
expect(innerConfig.setReasoningEffort).toHaveBeenLastCalledWith(
undefined,
);
expect(
reset.configOptions.find((option) => option.id === 'reasoning_effort')
?.currentValue,
).toBe('default');

await expect(
agent.setSessionConfigOption({
sessionId,
configId: 'reasoning_effort',
value: 'ultra',
}),
).rejects.toThrow(
'Unknown reasoning effort: ultra. Choose one of: default, low, medium, high, xhigh, max',
);
expect(innerConfig.setReasoningEffort).toHaveBeenCalledTimes(2);

innerConfig.setReasoningEffort.mockImplementation(() => {});
await expect(
agent.setSessionConfigOption({
sessionId,
configId: 'reasoning_effort',
value: 'xhigh',
}),
).rejects.toThrow(
'Reasoning effort cannot be applied while thinking is disabled',
);
expect(innerConfig.setReasoningEffort).toHaveBeenCalledTimes(3);
} finally {
mockConnectionState.resolve();
await agentPromise;
}
});

it.each([
{
description: 'hide discontinued qwen-oauth for other auth types',
Expand Down
55 changes: 54 additions & 1 deletion packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ import {
normalizeSnapshotPayload,
startEventLoopLagMonitor,
refreshMemoryInstruction,
applyReasoningEffort,
REASONING_EFFORT_TIERS,
extractDaemonTraceContext,
withDaemonSpan,
type AgentParams,
Expand All @@ -109,6 +111,7 @@ import {
type ProviderConfig,
type ProviderModelConfig,
type ProviderSetupInputs,
type ReasoningEffort,
type ResumedSessionData,
type SendSdkMcpMessage,
type SessionArtifactEventRecordPayload,
Expand Down Expand Up @@ -363,6 +366,14 @@ const POSIX_TMP_LOCAL_READ_ROOT = '/tmp';
const BTW_CHILD_TIMEOUT_MS = 55_000;
const MCP_OAUTH_START_TIMEOUT_MS = 30_000;
const SESSION_DRAIN_TIMEOUT_MS = 30_000;
const ACP_REASONING_EFFORT_DEFAULT = 'default';
const ACP_REASONING_EFFORT_NAMES: Record<ReasoningEffort, string> = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The new ACP_REASONING_EFFORT_NAMES map duplicates the per-tier display labels already defined for the same five tiers in the reasoningEffort enum options at packages/cli/src/config/settingsSchema.ts:1463 — and the two copies have already drifted in casing: xhigh: 'Extra high' here vs label: 'Extra High' in the schema (showInDialog: true). A third copy ("thinking.xhigh": "Extra High") lives in desktop/packages/shared/src/i18n/locales/en.json. The diff's own new test asserts the ACP spelling, so both spellings now ship. — Concrete cost: the same tier renders with different labels depending on surface (an ACP client such as JetBrains shows "Extra high" while the CLI settings dialog shows "Extra High"), and any future label edit or tier addition must be made in several independent places — missing one silently widens the divergence.

Suggested fix (spans multiple files, so no one-click suggestion): extract one shared tier→label record next to REASONING_EFFORT_TIERS in packages/core/src/core/reasoning-effort.ts and consume it from buildConfigOptions, the settings schema, and the desktop i18n copy, aligning the casing.

中文说明

新增的 ACP_REASONING_EFFORT_NAMES 映射重复定义了 packages/cli/src/config/settingsSchema.ts:1463reasoningEffort 枚举选项里同样五个档位已有的显示标签——且两份拷贝在大小写上已经出现漂移:此处为 xhigh: 'Extra high',schema 中为 label: 'Extra High'showInDialog: true)。第三份拷贝("thinking.xhigh": "Extra High")位于 desktop/packages/shared/src/i18n/locales/en.json。本 diff 新增的测试断言的是 ACP 侧拼写,因此两种拼写现在都会随产品发布。——具体代价:同一档位在不同界面显示不同标签(JetBrains 等 ACP 客户端显示 "Extra high",CLI 设置对话框显示 "Extra High");未来任何标签修改或新增档位都必须同时改多处独立代码,漏改其一会在界面之间悄悄扩大分歧。

建议修复(跨多文件,故不提供一键 suggestion):在 packages/core/src/core/reasoning-effort.tsREASONING_EFFORT_TIERS 旁提取一份共享的 档位→标签 记录,供 buildConfigOptions、settings schema 与 desktop i18n 拷贝共同消费,并统一大小写。

— qwen3.8-max via Qwen Code /review (v0.21.7)

low: 'Low',
medium: 'Medium',
high: 'High',
xhigh: 'Extra high',
max: 'Max',
};

// Must be less than WORKSPACE_MEMORY_REMEMBER_TIMEOUT_MS (300s) in bridge.ts.
const WORKSPACE_MEMORY_REMEMBER_CHILD_TIMEOUT_MS = 295_000;
Expand Down Expand Up @@ -5173,6 +5184,25 @@ class QwenAgent implements Agent {
);
break;
}
case 'reasoning_effort': {
const effort =
value === ACP_REASONING_EFFORT_DEFAULT
? undefined
: REASONING_EFFORT_TIERS.find((tier) => tier === value);
if (value !== ACP_REASONING_EFFORT_DEFAULT && effort === undefined) {
throw RequestError.invalidParams(
undefined,
`Unknown reasoning effort: ${value}. Choose one of: ${ACP_REASONING_EFFORT_DEFAULT}, ${REASONING_EFFORT_TIERS.join(', ')}`,
);
}
if (!applyReasoningEffort(session.getConfig(), effort)) {
throw RequestError.invalidParams(
undefined,
'Reasoning effort cannot be applied while thinking is disabled',
);
}
break;
}
default:
throw RequestError.invalidParams(
undefined,
Expand Down Expand Up @@ -11973,7 +12003,30 @@ class QwenAgent implements Agent {
options: configModelOptions,
};

return [modeConfigOption, modelConfigOption];
const reasoningEffortConfigOption: SessionConfigOption = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] buildConfigOptions advertises the reasoning_effort selector unconditionally, but when thinking is disabled (ContentGeneratorConfig.reasoning === false — a user-configurable model generation field) every listed non-default value deterministically fails with -32602 'Reasoning effort cannot be applied while thinking is disabled'. — Failure scenario: a session whose active model/user config disables thinking still receives reasoning_effort in session/new configOptions with currentValue: 'default' and five selectable tiers; the ACP client (JetBrains, per the docs bullet this PR adds) renders the selector from that list, the user picks e.g. high, and each tier pick reproduces the same error, with no advertised way to re-enable thinking. Sibling mode/model options list only settable values.

Suggested fix: omit the option or mark it unavailable when config.getContentGeneratorConfig()?.reasoning === false (which is distinguishable from an unset tier), so clients only render a selector that can succeed — or keep the advertise-always behavior deliberately and document it as such.

中文说明

buildConfigOptions 无条件地广播 reasoning_effort 选择器,但当 thinking 被禁用时(ContentGeneratorConfig.reasoning === false——这是用户可配置的模型生成字段),列表中除 default 外的每个取值都必然以 -32602 'Reasoning effort cannot be applied while thinking is disabled' 失败。——失败场景:活动模型/用户配置禁用了 thinking 的会话,仍然会在 session/newconfigOptions 中收到 reasoning_effortcurrentValue: 'default' 加五个可选档位);ACP 客户端(按本 PR 新增的文档条目,即 JetBrains)会据此渲染选择器,用户选择如 high 后每次都会得到同样的错误,且没有任何广播出来的途径可以重新启用 thinking。同级的 mode/model 选项只列出可设置的取值。

建议修复:当 config.getContentGeneratorConfig()?.reasoning === false 时省略该选项或将其标记为不可用(该状态与未设置档位可区分),使客户端只渲染能够成功的选择器——或有意保留“始终广播”的行为并在文档中注明。

— qwen3.8-max via Qwen Code /review (v0.21.7)

id: 'reasoning_effort',
name: 'Reasoning effort',
description: 'How hard reasoning-capable models should think',
category: 'thought_level',
type: 'select' as const,
currentValue:
config.getReasoningEffort?.() ?? ACP_REASONING_EFFORT_DEFAULT,
options: [
{
value: ACP_REASONING_EFFORT_DEFAULT,
name: 'Default',
description: 'Use the model or provider default',
},
...REASONING_EFFORT_TIERS.map((effort) => ({
value: effort,
name: ACP_REASONING_EFFORT_NAMES[effort],
description:
'Providers map or clamp the requested tier for the active model',
})),
Comment thread
zjunothing marked this conversation as resolved.
],
};

return [modeConfigOption, modelConfigOption, reasoningEffortConfigOption];
Comment thread
zjunothing marked this conversation as resolved.
}

private buildSelectableModelOptions(config: Config) {
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/acp-integration/acpAgent.worktree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ vi.mock('@qwen-code/qwen-code-core', () => ({
})),
APPROVAL_MODE_INFO: {},
APPROVAL_MODES: [],
applyReasoningEffort: (
config: {
setReasoningEffort(effort: string | undefined): void;
getReasoningEffort(): string | undefined;
},
effort: string | undefined,
) => {
config.setReasoningEffort(effort);
return config.getReasoningEffort() === effort;
},
REASONING_EFFORT_TIERS: ['low', 'medium', 'high', 'xhigh', 'max'],
DEFAULT_STOP_HOOK_BLOCK_CAP: 8,
DEFAULT_MAX_SUBAGENT_DEPTH: 5,
DEFAULT_MAX_TOOL_CALLS_PER_TURN: 100,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
createDebugLogger,
MCPServerConfig,
AuthProviderType,
applyReasoningEffort,
normalizeReasoningEffort,
loadUsageDashboard,
type MCPOAuthConfig,
Expand Down Expand Up @@ -176,16 +177,14 @@ export class SystemController extends BaseController {
const normalized = normalizeReasoningEffort(payload.effort);
if (normalized) {
try {
this.context.config.setReasoningEffort(normalized);

if (this.context.config.getReasoningEffort() !== normalized) {
debugLogger.warn(
`[SystemController] Effort '${normalized}' was not applied (thinking may be disabled)`,
);
} else {
if (applyReasoningEffort(this.context.config, normalized)) {
debugLogger.info(
`[SystemController] Set reasoning effort to: ${normalized}`,
);
} else {
debugLogger.warn(
`[SystemController] Effort '${normalized}' was not applied (thinking may be disabled)`,
);
}
} catch (error) {
debugLogger.error(
Expand Down Expand Up @@ -533,9 +532,7 @@ export class SystemController extends BaseController {
}

try {
this.context.config.setReasoningEffort(normalized);

const applied = this.context.config.getReasoningEffort() === normalized;
const applied = applyReasoningEffort(this.context.config, normalized);

debugLogger.info(
`[SystemController] Reasoning effort set to: ${normalized} (applied: ${applied})`,
Expand Down
Loading
Loading