Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,21 @@ export class ClaudeCodeSession extends Disposable {
}
}

/**
* Sets the reasoning effort on the active SDK session, or stores it for the next session start.
*/
private async _setEffort(effort: EffortLevel | undefined): Promise<void> {
if (effort === this._currentEffort) {
return;
}
this._currentEffort = effort;
if (this._queryGenerator) {
this.logService.trace(`[ClaudeCodeSession] Setting effort to ${effort} on active session`);
// Settings.effortLevel does not include 'max'; the SDK treats it as a 'high' fallback.
await this._queryGenerator.applyFlagSettings({ effortLevel: effort as 'low' | 'medium' | 'high' | 'xhigh' | undefined });
Comment thread
TylerLeonhardt marked this conversation as resolved.
}
}

constructor(
private readonly langModelServer: ClaudeLanguageModelServer,
public readonly sessionId: string,
Expand Down Expand Up @@ -520,16 +535,17 @@ export class ClaudeCodeSession extends Disposable {
}

// Check non-hot-swappable changes that require a session restart
if (request.effort !== this._currentEffort || !this._toolsMatch(request.toolsSnapshot)) {
if (!this._toolsMatch(request.toolsSnapshot)) {
this._queuedRequests.unshift(request);
this._pendingRestart = true;
this._isResumed = true;
return;
}

// Hot-swap model and permission mode on the active session
// Hot-swap model, permission mode, and effort on the active session
await this._setModel(request.modelId);
await this._setPermissionMode(request.permissionMode);
await this._setEffort(request.effort);

// Mark this request as yielded to the SDK; it becomes the current request.
this._inFlightRequests.push(request);
Expand Down Expand Up @@ -652,7 +668,7 @@ export class ClaudeCodeSession extends Disposable {
throw new Error('Session ended unexpectedly');
} catch (error) {
// Graceful restart: the prompt iterable detected a non-hot-swappable change
// (effort or tools). Preserve queued requests and start a fresh session.
// (tools). Preserve queued requests and start a fresh session.
if (this._pendingRestart) {
this._pendingRestart = false;
this._restartSession();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,10 +506,11 @@ describe('ClaudeCodeSession', () => {
expect(mockService.lastQueryOptions?.effort).toBeUndefined();
});

it('restarts session when effort level changes', async () => {
it('calls applyFlagSettings when effort level changes instead of restarting session', async () => {
const mockServer = createMockLangModelServer();
const mockService = instantiationService.invokeFunction(accessor => accessor.get(IClaudeCodeSdkService)) as MockClaudeCodeSdkService;
mockService.queryCallCount = 0;
mockService.applyFlagSettingsCallCount = 0;

commitTestState(sessionStateService, 'test-session', TEST_MODEL_ID);
const session = store.add(instantiationService.createInstance(ClaudeCodeSession, mockServer, 'test-session', true));
Expand All @@ -522,16 +523,19 @@ describe('ClaudeCodeSession', () => {
// Change effort level
sessionStateService.setReasoningEffortForSession('test-session', 'high');

// Second request should restart session (new query created)
// Second request should hot-swap effort, not restart the session
const stream2 = new MockChatResponseStream();
await session.invoke(createMockChatRequest('Hello again'), stream2, undefined, CancellationToken.None);
expect(mockService.queryCallCount).toBe(2);
expect(mockService.queryCallCount).toBe(1);
expect(mockService.applyFlagSettingsCallCount).toBe(1);
expect(mockService.lastAppliedFlagSettings).toEqual({ effortLevel: 'high' });
});

it('does not restart session when effort level is unchanged', async () => {
it('does not call applyFlagSettings when effort level is unchanged', async () => {
const mockServer = createMockLangModelServer();
const mockService = instantiationService.invokeFunction(accessor => accessor.get(IClaudeCodeSdkService)) as MockClaudeCodeSdkService;
mockService.queryCallCount = 0;
mockService.applyFlagSettingsCallCount = 0;

commitTestState(sessionStateService, 'test-session', TEST_MODEL_ID);
sessionStateService.setReasoningEffortForSession('test-session', 'medium');
Expand All @@ -546,6 +550,7 @@ describe('ClaudeCodeSession', () => {
const stream2 = new MockChatResponseStream();
await session.invoke(createMockChatRequest('Hello again'), stream2, undefined, CancellationToken.None);
expect(mockService.queryCallCount).toBe(1);
expect(mockService.applyFlagSettingsCallCount).toBe(0);
});
});

Expand Down Expand Up @@ -733,7 +738,7 @@ describe('ClaudeCodeSession - settings change restart', () => {
});
});

describe('ClaudeCodeSession - effort and tools restart', () => {
describe('ClaudeCodeSession - tools restart', () => {
const store = new DisposableStore();
let instantiationService: IInstantiationService;
let sessionStateService: IClaudeSessionStateService;
Expand All @@ -753,7 +758,7 @@ describe('ClaudeCodeSession - effort and tools restart', () => {
vi.resetAllMocks();
});

it('uses resume after effort change restart', async () => {
it('hot-swaps effort instead of restarting the session', async () => {
const mockServer = createMockLangModelServer();
commitTestState(sessionStateService, 'test-session');
const session = store.add(instantiationService.createInstance(ClaudeCodeSession, mockServer, 'test-session', true));
Expand All @@ -762,15 +767,17 @@ describe('ClaudeCodeSession - effort and tools restart', () => {
const stream1 = new MockChatResponseStream();
await session.invoke(createMockChatRequest('Hello'), stream1, undefined, CancellationToken.None);
expect(mockService.lastQueryOptions?.sessionId).toBe('test-session');
expect(mockService.queryCallCount).toBe(1);

// Change effort
sessionStateService.setReasoningEffortForSession('test-session', 'high');

// Restarted session should use resume
// Same query is reused via applyFlagSettings
const stream2 = new MockChatResponseStream();
await session.invoke(createMockChatRequest('Hello again'), stream2, undefined, CancellationToken.None);
expect(mockService.lastQueryOptions?.resume).toBe('test-session');
expect(mockService.lastQueryOptions?.effort).toBe('high');
expect(mockService.queryCallCount).toBe(1);
expect(mockService.applyFlagSettingsCallCount).toBe(1);
expect(mockService.lastAppliedFlagSettings).toEqual({ effortLevel: 'high' });
});

it('restarts session when MCP tools change', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { ForkSessionOptions, ForkSessionResult, GetSubagentMessagesOptions, ListSubagentsOptions, Options, Query, SDKAssistantMessage, SDKResultMessage, SDKSessionInfo, SDKUserMessage, SessionMessage } from '@anthropic-ai/claude-agent-sdk';
import type { ForkSessionOptions, ForkSessionResult, GetSubagentMessagesOptions, ListSubagentsOptions, Options, Query, SDKAssistantMessage, SDKResultMessage, SDKSessionInfo, SDKUserMessage, SessionMessage, Settings } from '@anthropic-ai/claude-agent-sdk';
import type { IClaudeCodeSdkService } from '../claudeCodeSdkService';

/**
Expand All @@ -16,6 +16,8 @@ export class MockClaudeCodeSdkService implements IClaudeCodeSdkService {
public lastSetModel: string | undefined;
public setPermissionModeCallCount = 0;
public lastSetPermissionMode: string | undefined;
public applyFlagSettingsCallCount = 0;
public lastAppliedFlagSettings: Settings | undefined;
public lastQueryOptions: Options | undefined;
public readonly receivedMessages: SDKUserMessage[] = [];

Expand Down Expand Up @@ -82,6 +84,10 @@ export class MockClaudeCodeSdkService implements IClaudeCodeSdkService {
this.setPermissionModeCallCount++;
this.lastSetPermissionMode = mode;
},
applyFlagSettings: async (settings: Settings) => {
this.applyFlagSettingsCallCount++;
this.lastAppliedFlagSettings = settings;
},
abort: () => { /* no-op for mock */ },
} as unknown as Query;
}
Expand Down
Loading