Skip to content

Commit 0453233

Browse files
authored
feat: Add effort level control for Claude sessions (#1211)
1. Add effort config option (low/medium/high/max) to Claude adapter session options 2. Wire effort parameter through agent service, session schemas and tRPC start session input 3. Pass effort level to session initialization and config change handler
1 parent fb49197 commit 0453233

File tree

10 files changed

+71
-16
lines changed

10 files changed

+71
-16
lines changed

apps/code/src/main/services/agent/schemas.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import type {
22
RequestPermissionRequest,
33
PermissionOption as SdkPermissionOption,
44
} from "@agentclientprotocol/sdk";
5+
import { effortLevelSchema } from "@shared/types.js";
56
import { z } from "zod";
67

8+
export { effortLevelSchema };
9+
export type { EffortLevel } from "@shared/types.js";
10+
711
// Session credentials schema
812
export const credentialsSchema = z.object({
913
apiKey: z.string(),
@@ -46,6 +50,7 @@ export const startSessionInput = z.object({
4650
adapter: z.enum(["claude", "codex"]).optional(),
4751
additionalDirectories: z.array(z.string()).optional(),
4852
customInstructions: z.string().max(2000).optional(),
53+
effort: effortLevelSchema.optional(),
4954
});
5055

5156
export type StartSessionInput = z.infer<typeof startSessionInput>;
@@ -162,6 +167,7 @@ export const reconnectSessionInput = z.object({
162167
additionalDirectories: z.array(z.string()).optional(),
163168
permissionMode: z.string().optional(),
164169
customInstructions: z.string().max(2000).optional(),
170+
effort: effortLevelSchema.optional(),
165171
});
166172

167173
export type ReconnectSessionInput = z.infer<typeof reconnectSessionInput>;

apps/code/src/main/services/agent/service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
AgentServiceEvent,
3939
type AgentServiceEvents,
4040
type Credentials,
41+
type EffortLevel,
4142
type InterruptReason,
4243
type PromptOutput,
4344
type ReconnectSessionInput,
@@ -200,6 +201,8 @@ interface SessionConfig {
200201
permissionMode?: string;
201202
/** Custom instructions injected into the system prompt */
202203
customInstructions?: string;
204+
/** Effort level for Claude sessions */
205+
effort?: EffortLevel;
203206
}
204207

205208
interface ManagedSession {
@@ -632,6 +635,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
632635
additionalDirectories,
633636
permissionMode,
634637
customInstructions,
638+
effort,
635639
} = config;
636640

637641
// Preview sessions don't need a real repo — use a temp directory
@@ -783,6 +787,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
783787
...(additionalDirectories?.length && {
784788
additionalDirectories,
785789
}),
790+
...(effort && { effort }),
786791
plugins,
787792
},
788793
},
@@ -811,6 +816,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
811816
claudeCode: {
812817
options: {
813818
...(additionalDirectories?.length && { additionalDirectories }),
819+
...(effort && { effort }),
814820
plugins,
815821
},
816822
},
@@ -1551,6 +1557,7 @@ For git operations while detached:
15511557
"permissionMode" in params ? params.permissionMode : undefined,
15521558
customInstructions:
15531559
"customInstructions" in params ? params.customInstructions : undefined,
1560+
effort: "effort" in params ? params.effort : undefined,
15541561
};
15551562
}
15561563

apps/code/src/renderer/features/message-editor/components/EditorToolbar.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { ModelSelector } from "@features/sessions/components/ModelSelector";
2-
import { ReasoningLevelSelector } from "@features/sessions/components/ReasoningLevelSelector";
32
import { Paperclip } from "@phosphor-icons/react";
43
import { Flex, IconButton, Tooltip } from "@radix-ui/themes";
54
import { useRef } from "react";
@@ -68,14 +67,7 @@ export function EditorToolbar({
6867
</IconButton>
6968
</Tooltip>
7069
{!hideSelectors && (
71-
<>
72-
<ModelSelector
73-
taskId={taskId}
74-
adapter={adapter}
75-
disabled={disabled}
76-
/>
77-
<ReasoningLevelSelector taskId={taskId} disabled={disabled} />
78-
</>
70+
<ModelSelector taskId={taskId} adapter={adapter} disabled={disabled} />
7971
)}
8072
</Flex>
8173
);

apps/code/src/renderer/features/sessions/components/ReasoningLevelSelector.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Select, Text } from "@radix-ui/themes";
22
import { getSessionService } from "../service/service";
33
import {
44
flattenSelectOptions,
5+
useAdapterForTask,
56
useSessionForTask,
67
useThoughtLevelConfigOptionForTask,
78
} from "../stores/sessionStore";
@@ -17,6 +18,7 @@ export function ReasoningLevelSelector({
1718
}: ReasoningLevelSelectorProps) {
1819
const session = useSessionForTask(taskId);
1920
const thoughtOption = useThoughtLevelConfigOptionForTask(taskId);
21+
const adapter = useAdapterForTask(taskId);
2022

2123
if (!thoughtOption) {
2224
return null;
@@ -57,7 +59,7 @@ export function ReasoningLevelSelector({
5759
}}
5860
>
5961
<Text size="1" style={{ fontFamily: "var(--font-mono)" }}>
60-
Reasoning: {activeLabel}
62+
{adapter === "codex" ? "Reasoning" : "Effort"}: {activeLabel}
6163
</Text>
6264
</Select.Trigger>
6365
<Select.Content position="popper" sideOffset={4}>

apps/code/src/renderer/features/sessions/service/service.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ import { getIsOnline } from "@renderer/stores/connectivityStore";
3030
import { trpcClient } from "@renderer/trpc/client";
3131
import { toast } from "@renderer/utils/toast";
3232
import { getCloudUrlFromRegion } from "@shared/constants/oauth";
33-
import type {
34-
CloudTaskUpdatePayload,
35-
ExecutionMode,
36-
Task,
33+
import {
34+
type CloudTaskUpdatePayload,
35+
type EffortLevel,
36+
type ExecutionMode,
37+
effortLevelSchema,
38+
type Task,
3739
} from "@shared/types";
3840
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
3941
import type { AcpMessage, StoredLogEntry } from "@shared/types/session-events";
@@ -539,6 +541,9 @@ export class SessionService {
539541
permissionMode: executionMode,
540542
adapter,
541543
customInstructions: startCustomInstructions || undefined,
544+
effort: effortLevelSchema.safeParse(reasoningLevel).success
545+
? (reasoningLevel as EffortLevel)
546+
: undefined,
542547
});
543548

544549
const session = this.createBaseSession(taskRun.id, taskId, taskTitle);

apps/code/src/renderer/features/task-detail/components/TaskInputEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export const TaskInputEditor = forwardRef<
210210
</Flex>
211211

212212
<Flex justify="between" align="center" px="3" pb="3">
213-
<Flex align="center" gap="1">
213+
<Flex align="center" gap="3">
214214
<EditorToolbar
215215
disabled={isCreatingTask}
216216
adapter={adapter}

apps/code/src/shared/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const executionModeSchema = z.enum([
1010
]);
1111
export type ExecutionMode = z.infer<typeof executionModeSchema>;
1212

13+
// Effort level schema and type - shared between main and renderer
14+
export const effortLevelSchema = z.enum(["low", "medium", "high", "max"]);
15+
export type EffortLevel = z.infer<typeof effortLevelSchema>;
16+
1317
interface UserBasic {
1418
id: number;
1519
uuid: string;

packages/agent/src/adapters/claude/claude-agent.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373
} from "./tools.js";
7474
import type {
7575
BackgroundTerminal,
76+
EffortLevel,
7677
NewSessionMeta,
7778
Session,
7879
ToolUseCache,
@@ -558,6 +559,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
558559
const sdkModelId = toSdkModelId(params.value);
559560
await this.session.query.setModel(sdkModelId);
560561
this.session.modelId = params.value;
562+
} else if (params.configId === "effort") {
563+
const newEffort = params.value as EffortLevel;
564+
this.session.effort = newEffort;
565+
this.session.queryOptions.effort = newEffort;
561566
}
562567

563568
this.session.configOptions = this.session.configOptions.map((o) =>
@@ -623,6 +628,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
623628

624629
const meta = params._meta as NewSessionMeta | undefined;
625630
const taskId = meta?.persistence?.taskId;
631+
const effort = meta?.claudeCode?.options?.effort as EffortLevel | undefined;
626632

627633
// We want to create a new session id unless it is resume,
628634
// but not resume + forkSession.
@@ -673,6 +679,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
673679
onModeChange: this.createOnModeChange(),
674680
onProcessSpawned: this.options?.onProcessSpawned,
675681
onProcessExited: this.options?.onProcessExited,
682+
effort,
676683
});
677684

678685
// Use the same abort controller that buildSessionOptions gave to the query
@@ -682,6 +689,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
682689

683690
const session: Session = {
684691
query: q,
692+
queryOptions: options,
685693
input,
686694
cancelled: false,
687695
settingsManager,
@@ -693,6 +701,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
693701
cachedReadTokens: 0,
694702
cachedWriteTokens: 0,
695703
},
704+
effort,
696705
configOptions: [],
697706
promptRunning: false,
698707
pendingMessages: new Map(),
@@ -790,7 +799,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
790799
),
791800
};
792801

793-
const configOptions = this.buildConfigOptions(permissionMode, modelOptions);
802+
const configOptions = this.buildConfigOptions(
803+
permissionMode,
804+
modelOptions,
805+
effort ?? "high",
806+
);
794807
session.configOptions = configOptions;
795808

796809
if (!creationOpts.skipBackgroundFetches) {
@@ -844,6 +857,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
844857
currentModelId: string;
845858
options: SessionConfigSelectOption[];
846859
},
860+
currentEffort: EffortLevel = "high",
847861
): SessionConfigOption[] {
848862
const modeOptions = getAvailableModes().map((mode) => ({
849863
value: mode.id,
@@ -871,6 +885,20 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
871885
category: "model" as SessionConfigOptionCategory,
872886
description: "Choose which model Claude should use",
873887
},
888+
{
889+
id: "effort",
890+
name: "Effort",
891+
type: "select",
892+
currentValue: currentEffort,
893+
options: [
894+
{ value: "low", name: "Low" },
895+
{ value: "medium", name: "Medium" },
896+
{ value: "high", name: "High" },
897+
{ value: "max", name: "Max" },
898+
],
899+
category: "thought_level" as SessionConfigOptionCategory,
900+
description: "Controls how much effort Claude puts into its response",
901+
},
874902
];
875903
}
876904

packages/agent/src/adapters/claude/session/options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
type OnModeChange,
1818
} from "../hooks.js";
1919
import type { CodeExecutionMode } from "../tools.js";
20+
import type { EffortLevel } from "../types.js";
2021
import { DEFAULT_MODEL } from "./models.js";
2122
import type { SettingsManager } from "./settings.js";
2223

@@ -43,6 +44,7 @@ export interface BuildOptionsParams {
4344
onModeChange?: OnModeChange;
4445
onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
4546
onProcessExited?: (pid: number) => void;
47+
effort?: EffortLevel;
4648
}
4749

4850
const BRANCH_NAMING_INSTRUCTIONS = `
@@ -295,6 +297,10 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
295297
options.additionalDirectories = params.additionalDirectories;
296298
}
297299

300+
if (params.effort) {
301+
options.effort = params.effort;
302+
}
303+
298304
clearStatsigCache();
299305
return options;
300306
}

packages/agent/src/adapters/claude/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import type { BaseSession } from "../base-acp-agent.js";
1313
import type { SettingsManager } from "./session/settings.js";
1414
import type { CodeExecutionMode } from "./tools.js";
1515

16+
export type EffortLevel = "low" | "medium" | "high" | "max";
17+
1618
export type AccumulatedUsage = {
1719
inputTokens: number;
1820
outputTokens: number;
@@ -38,6 +40,8 @@ export type PendingMessage = {
3840

3941
export type Session = BaseSession & {
4042
query: Query;
43+
/** The Options object passed to query() — mutating it affects subsequent prompts */
44+
queryOptions: Options;
4145
input: Pushable<SDKUserMessage>;
4246
settingsManager: SettingsManager;
4347
permissionMode: CodeExecutionMode;
@@ -46,6 +50,7 @@ export type Session = BaseSession & {
4650
taskRunId?: string;
4751
lastPlanFilePath?: string;
4852
lastPlanContent?: string;
53+
effort?: EffortLevel;
4954
configOptions: SessionConfigOption[];
5055
accumulatedUsage: AccumulatedUsage;
5156
promptRunning: boolean;

0 commit comments

Comments
 (0)