feat(claude): add effort setting parity with model across stack (#353)

Co-authored-by: Xiaoyi <xiaoyizhang@microsoft.com>
This commit is contained in:
xyzhang626
2026-03-24 21:15:48 +08:00
committed by GitHub
co-authored by Xiaoyi
parent 30265fdc25
commit a200fe9628
50 changed files with 715 additions and 52 deletions
+1
View File
@@ -125,6 +125,7 @@ export async function claudeRemote(opts: {
mcpServers: opts.mcpServers,
permissionMode: initial.mode.permissionMode,
model: initial.mode.model,
effort: initial.mode.effort,
fallbackModel: initial.mode.fallbackModel,
customSystemPrompt: initial.mode.customSystemPrompt ? initial.mode.customSystemPrompt + '\n\n' + systemPrompt : undefined,
appendSystemPrompt: initial.mode.appendSystemPrompt ? initial.mode.appendSystemPrompt + '\n\n' + systemPrompt : systemPrompt,
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { normalizeClaudeSessionEffort } from './effort'
describe('normalizeClaudeSessionEffort', () => {
it('returns null when effort is missing', () => {
expect(normalizeClaudeSessionEffort()).toBeNull()
})
it('returns null for auto-like values', () => {
expect(normalizeClaudeSessionEffort('')).toBeNull()
expect(normalizeClaudeSessionEffort('auto')).toBeNull()
expect(normalizeClaudeSessionEffort('default')).toBeNull()
expect(normalizeClaudeSessionEffort(' AUTO ')).toBeNull()
})
it('normalizes supported effort values', () => {
expect(normalizeClaudeSessionEffort('medium')).toBe('medium')
expect(normalizeClaudeSessionEffort('high')).toBe('high')
expect(normalizeClaudeSessionEffort('max')).toBe('max')
expect(normalizeClaudeSessionEffort(' High ')).toBe('high')
})
})
+10
View File
@@ -0,0 +1,10 @@
import type { SessionEffort } from '@/api/types'
export function normalizeClaudeSessionEffort(effort?: string | null): SessionEffort {
const trimmedEffort = effort?.trim().toLowerCase()
if (!trimmedEffort || trimmedEffort === 'auto' || trimmedEffort === 'default') {
return null
}
return trimmedEffort
}
+5 -2
View File
@@ -6,7 +6,7 @@ import { Session } from "./session"
import { claudeLocalLauncher } from "./claudeLocalLauncher"
import { claudeRemoteLauncher } from "./claudeRemoteLauncher"
import { ApiClient } from "@/lib"
import type { SessionModel } from "@/api/types"
import type { SessionEffort, SessionModel } from "@/api/types"
import type { ClaudePermissionMode } from "@hapi/protocol/types"
export type PermissionMode = ClaudePermissionMode;
@@ -14,6 +14,7 @@ export type PermissionMode = ClaudePermissionMode;
export interface EnhancedMode {
permissionMode: PermissionMode;
model?: string;
effort?: string;
fallbackModel?: string;
customSystemPrompt?: string;
appendSystemPrompt?: string;
@@ -24,6 +25,7 @@ export interface EnhancedMode {
interface LoopOptions {
path: string
model?: SessionModel
effort?: SessionEffort
permissionMode?: PermissionMode
startingMode?: 'local' | 'remote'
startedBy?: 'runner' | 'terminal'
@@ -62,7 +64,8 @@ export async function loop(opts: LoopOptions) {
startingMode,
hookSettingsPath: opts.hookSettingsPath,
permissionMode: opts.permissionMode ?? 'default',
model: opts.model
model: opts.model,
effort: opts.effort
});
await runLocalRemoteSession({
+38 -6
View File
@@ -1,6 +1,6 @@
import { logger } from '@/ui/logger';
import { loop } from '@/claude/loop';
import { AgentState, SessionModel } from '@/api/types';
import { AgentState, SessionEffort, SessionModel } from '@/api/types';
import { EnhancedMode, PermissionMode } from './loop';
import { MessageQueue2 } from '@/utils/MessageQueue2';
import { hashObject } from '@/utils/deterministicJson';
@@ -18,10 +18,12 @@ import { isPermissionModeAllowedForFlavor } from '@hapi/protocol';
import { PermissionModeSchema } from '@hapi/protocol/schemas';
import { formatMessageWithAttachments } from '@/utils/attachmentFormatter';
import { normalizeClaudeSessionModel } from './model';
import { normalizeClaudeSessionEffort } from './effort';
import { getInvokedCwd } from '@/utils/invokedCwd';
export interface StartOptions {
model?: string
effort?: string
permissionMode?: PermissionMode
startingMode?: 'local' | 'remote'
shouldStartRunner?: boolean
@@ -48,12 +50,14 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
const initialState: AgentState = {};
const initialModel = normalizeClaudeSessionModel(options.model);
const initialEffort = normalizeClaudeSessionEffort(options.effort);
const { api, session, sessionInfo } = await bootstrapSession({
flavor: 'claude',
startedBy,
workingDirectory,
agentState: initialState,
model: initialModel ?? undefined
model: initialModel ?? undefined,
effort: initialEffort ?? undefined
});
logger.debug(`Session created: ${sessionInfo.id}`);
@@ -138,6 +142,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
const messageQueue = new MessageQueue2<EnhancedMode>(mode => hashObject({
isPlan: mode.permissionMode === 'plan',
model: mode.model,
effort: mode.effort,
fallbackModel: mode.fallbackModel,
customSystemPrompt: mode.customSystemPrompt,
appendSystemPrompt: mode.appendSystemPrompt,
@@ -148,6 +153,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
// Forward messages to the queue
let currentPermissionMode: PermissionMode = options.permissionMode ?? 'default';
let currentModel: SessionModel = initialModel;
let currentEffort: SessionEffort = initialEffort;
let currentFallbackModel: string | undefined = undefined; // Track current fallback model
let currentCustomSystemPrompt: string | undefined = undefined; // Track current custom system prompt
let currentAppendSystemPrompt: string | undefined = undefined; // Track current append system prompt
@@ -161,7 +167,8 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
}
sessionInstance.setPermissionMode(currentPermissionMode);
sessionInstance.setModel(currentModel);
logger.debug(`[loop] Synced session config for keepalive: permissionMode=${currentPermissionMode}, model=${currentModel ?? 'auto'}`);
sessionInstance.setEffort(currentEffort);
logger.debug(`[loop] Synced session config for keepalive: permissionMode=${currentPermissionMode}, model=${currentModel ?? 'auto'}, effort=${currentEffort ?? 'auto'}`);
};
session.onUserMessage((message) => {
const sessionPermissionMode = currentSessionRef.current?.getPermissionMode();
@@ -172,9 +179,14 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
if (sessionModel !== undefined) {
currentModel = sessionModel;
}
const sessionEffort = currentSessionRef.current?.getEffort();
if (sessionEffort !== undefined) {
currentEffort = sessionEffort;
}
const messagePermissionMode = currentPermissionMode;
const messageModel = currentModel ?? undefined;
logger.debug(`[loop] User message received with permission mode: ${currentPermissionMode}, model: ${currentModel ?? 'auto'}`);
const messageEffort = currentEffort ?? undefined;
logger.debug(`[loop] User message received with permission mode: ${currentPermissionMode}, model: ${currentModel ?? 'auto'}, effort: ${currentEffort ?? 'auto'}`);
// Resolve custom system prompt - use message.meta.customSystemPrompt if provided, otherwise use current
let messageCustomSystemPrompt = currentCustomSystemPrompt;
@@ -237,6 +249,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
const enhancedMode: EnhancedMode = {
permissionMode: messagePermissionMode ?? 'default',
model: messageModel,
effort: messageEffort,
fallbackModel: messageFallbackModel,
customSystemPrompt: messageCustomSystemPrompt,
appendSystemPrompt: messageAppendSystemPrompt,
@@ -255,6 +268,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
const enhancedMode: EnhancedMode = {
permissionMode: messagePermissionMode ?? 'default',
model: messageModel,
effort: messageEffort,
fallbackModel: messageFallbackModel,
customSystemPrompt: messageCustomSystemPrompt,
appendSystemPrompt: messageAppendSystemPrompt,
@@ -272,6 +286,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
const enhancedMode: EnhancedMode = {
permissionMode: messagePermissionMode ?? 'default',
model: messageModel,
effort: messageEffort,
fallbackModel: messageFallbackModel,
customSystemPrompt: messageCustomSystemPrompt,
appendSystemPrompt: messageAppendSystemPrompt,
@@ -302,11 +317,23 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
return normalizeClaudeSessionModel(value);
};
const resolveEffort = (value: unknown): SessionEffort => {
if (value === null) {
return null;
}
if (typeof value !== 'string') {
throw new Error('Invalid effort');
}
return normalizeClaudeSessionEffort(value);
};
session.rpcHandlerManager.registerHandler('set-session-config', async (payload: unknown) => {
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid session config payload');
}
const config = payload as { permissionMode?: unknown; model?: unknown };
const config = payload as { permissionMode?: unknown; model?: unknown; effort?: unknown };
if (config.permissionMode !== undefined) {
currentPermissionMode = resolvePermissionMode(config.permissionMode);
@@ -316,8 +343,12 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
currentModel = resolveModel(config.model);
}
if (config.effort !== undefined) {
currentEffort = resolveEffort(config.effort);
}
syncSessionModes();
return { applied: { permissionMode: currentPermissionMode, model: currentModel } };
return { applied: { permissionMode: currentPermissionMode, model: currentModel, effort: currentEffort } };
});
let loopError: unknown = null;
@@ -326,6 +357,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
await loop({
path: workingDirectory,
model: currentModel,
effort: currentEffort,
permissionMode: options.permissionMode,
startingMode,
messageQueue,
+2
View File
@@ -280,6 +280,7 @@ export function query(config: {
continue: continueConversation,
resume,
model,
effort,
fallbackModel,
settingsPath,
strictMcpConfig,
@@ -300,6 +301,7 @@ export function query(config: {
if (appendSystemPrompt) args.push('--append-system-prompt', stripNewlinesForWindowsShellArg(appendSystemPrompt))
if (maxTurns) args.push('--max-turns', maxTurns.toString())
if (model) args.push('--model', model)
if (effort) args.push('--effort', effort)
if (canCallTool) {
if (typeof prompt === 'string') {
throw new Error('canCallTool callback requires --input-format stream-json. Please set prompt as an AsyncIterable.')
+1
View File
@@ -170,6 +170,7 @@ export interface QueryOptions {
continue?: boolean
resume?: string
model?: string
effort?: string
fallbackModel?: string
settingsPath?: string
strictMcpConfig?: boolean
+9 -2
View File
@@ -2,7 +2,7 @@ import { ApiClient, ApiSessionClient } from '@/lib';
import { MessageQueue2 } from '@/utils/MessageQueue2';
import { logger } from '@/ui/logger';
import { AgentSessionBase } from '@/agent/sessionBase';
import type { SessionModel } from '@/api/types';
import type { SessionEffort, SessionModel } from '@/api/types';
import type { EnhancedMode } from './loop';
import type { PermissionMode } from './loop';
import type { LocalLaunchExitReason } from '@/agent/localLaunchPolicy';
@@ -40,6 +40,7 @@ export class Session extends AgentSessionBase<EnhancedMode> {
hookSettingsPath: string;
permissionMode?: PermissionMode;
model?: SessionModel;
effort?: SessionEffort;
}) {
super({
api: opts.api,
@@ -57,7 +58,8 @@ export class Session extends AgentSessionBase<EnhancedMode> {
claudeSessionId: sessionId
}),
permissionMode: opts.permissionMode,
model: opts.model
model: opts.model,
effort: opts.effort
});
this.claudeEnvVars = opts.claudeEnvVars;
@@ -69,6 +71,7 @@ export class Session extends AgentSessionBase<EnhancedMode> {
this.startingMode = opts.startingMode;
this.permissionMode = opts.permissionMode;
this.model = opts.model;
this.effort = opts.effort;
}
setPermissionMode = (mode: PermissionMode): void => {
@@ -79,6 +82,10 @@ export class Session extends AgentSessionBase<EnhancedMode> {
this.model = model;
};
setEffort = (effort: SessionEffort): void => {
this.effort = effort;
};
recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
this.localLaunchFailure = { message, exitReason };
};