mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(claude): add effort setting parity with model across stack (#353)
Co-authored-by: Xiaoyi <xiaoyizhang@microsoft.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -170,6 +170,7 @@ export interface QueryOptions {
|
||||
continue?: boolean
|
||||
resume?: string
|
||||
model?: string
|
||||
effort?: string
|
||||
fallbackModel?: string
|
||||
settingsPath?: string
|
||||
strictMcpConfig?: boolean
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user