Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/session-thinking-effort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Add the `--effort` flag for session-level thinking overrides. Pass `--effort <value>` when starting or resuming a session.
2 changes: 2 additions & 0 deletions apps/kimi-code/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function createProgram(
'LLM model alias to use for this invocation. Defaults to default_model in config.toml.',
),
)
.option('--effort <effort>', 'Set the thinking effort for this session')
.addOption(
new Option(
'-p, --prompt <prompt>',
Expand Down Expand Up @@ -157,6 +158,7 @@ export function createProgram(
auto: autoValue,
plan: raw['plan'] as boolean,
model: raw['model'] as string | undefined,
effort: raw['effort'] as string | undefined,
outputFormat: raw['outputFormat'] as CLIOptions['outputFormat'],
prompt: raw['prompt'] as string | undefined,
skillsDirs: raw['skillsDir'] as string[],
Expand Down
4 changes: 4 additions & 0 deletions apps/kimi-code/src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface CLIOptions {
auto: boolean;
plan: boolean;
model: string | undefined;
effort: string | undefined;
outputFormat: PromptOutputFormat | undefined;
prompt: string | undefined;
skillsDirs: string[];
Expand Down Expand Up @@ -73,6 +74,9 @@ export function validateOptions(
if (opts.model !== undefined && opts.model.trim().length === 0) {
throw new OptionConflictError('Model cannot be empty.');
}
if (opts.effort !== undefined && opts.effort.trim().length === 0) {
throw new OptionConflictError('Effort cannot be empty.');
}
if (!promptMode && opts.outputFormat !== undefined) {
throw new OptionConflictError('Output format is only supported in prompt mode.');
}
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/cli/prompt-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface PromptSession {

getStatus(): Promise<SessionStatus>;
setModel(model: string): Promise<void>;
setThinking(effort: string): Promise<void>;
setPermission(mode: PermissionMode): Promise<void>;
setApprovalHandler(handler: ApprovalHandler | undefined): void;
setQuestionHandler(handler: QuestionHandler | undefined): void;
Expand Down
23 changes: 17 additions & 6 deletions apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,15 @@ interface ResolvedPromptSession {
readonly goalModel?: string;
}

async function applySessionOverrides(session: PromptSession, opts: CLIOptions): Promise<void> {
if (opts.model !== undefined) {
await session.setModel(opts.model);
}
if (opts.effort !== undefined) {
await session.setThinking(opts.effort);
}
}

async function resolvePromptSession(
harness: PromptHarness,
opts: CLIOptions,
Expand Down Expand Up @@ -326,9 +335,7 @@ async function resolvePromptSession(
status.permission,
setRestorePermission,
);
if (opts.model !== undefined) {
await session.setModel(opts.model);
}
await applySessionOverrides(session, opts);
installHeadlessHandlers(session);
return {
session,
Expand All @@ -353,9 +360,7 @@ async function resolvePromptSession(
status.permission,
setRestorePermission,
);
if (opts.model !== undefined) {
await session.setModel(opts.model);
}
await applySessionOverrides(session, opts);
installHeadlessHandlers(session);
return {
session,
Expand All @@ -373,12 +378,18 @@ async function resolvePromptSession(
const session = await harness.createSession({
workDir,
model,
thinking: opts.effort,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Strictly validate effort on fresh sessions

For a fresh session, forwarding the value through createSession does not provide the model-specific validation promised by this flag: the v1 create path calls resolveThinkingEffort without strict Kimi-protocol validation, while the fresh v2 binding likewise omits strictThinking, so an unsupported value can be retained for a later upstream failure or silently normalized instead of rejecting the invocation. Resumed sessions do call setThinking and reject the same input, making behavior depend on whether the session already exists; fresh prompt, TUI, and native-v2 creation should use the strict setter/binding semantics too.

Useful? React with 👍 / 👎.

permission: 'auto',
additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined,
agentProfile,
agentFiles: opts.agentFiles?.length ? opts.agentFiles : undefined,
drainAgentTasksOnStop: true,
});
// Session creation preserves the SDK's lenient compatibility semantics.
// Explicit CLI input must additionally use the strict, model-aware setter.
if (opts.effort !== undefined) {
await session.setThinking(opts.effort);
}
installHeadlessHandlers(session);
return {
session,
Expand Down
15 changes: 10 additions & 5 deletions apps/kimi-code/src/cli/v2/run-v2-print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,16 @@ async function resolveNativeSession(
}

// `--agent` / `--agent-file` are creation-only: validateOptions rejects them
// together with --session/--continue, so resume paths only apply an
// explicitly requested model — the bound profile is restored by the engine.
const applyModelOverride = async (
// together with --session/--continue, so resume paths only apply explicit
// model/thinking overrides — the bound profile is restored by the engine.
// Model must be applied first because effort is validated against it.
const applyProfileOverrides = async (
profile: IAgentProfileService,
model: string | undefined,
effort: string | undefined,
): Promise<void> => {
if (model !== undefined) await profile.setModel(model);
if (effort !== undefined) profile.setThinking(effort);
};

const resumeById = async (id: string): Promise<ISessionScopeHandle> => {
Expand Down Expand Up @@ -344,7 +347,7 @@ async function resolveNativeSession(
const session = await resumeById(opts.session);
const agent = await ensureMainAgent(session);
const profile = agent.accessor.get(IAgentProfileService);
await applyModelOverride(profile, opts.model);
await applyProfileOverrides(profile, opts.model, opts.effort);
const currentModel = profile.getModel();
const { restorePermission } = forceAuto(agent);
return {
Expand All @@ -363,7 +366,7 @@ async function resolveNativeSession(
const session = await resumeById(previous.id);
const agent = await ensureMainAgent(session);
const profile = agent.accessor.get(IAgentProfileService);
await applyModelOverride(profile, opts.model);
await applyProfileOverrides(profile, opts.model, opts.effort);
const currentModel = profile.getModel();
const { restorePermission } = forceAuto(agent);
return {
Expand All @@ -385,6 +388,8 @@ async function resolveNativeSession(
mainAgentBinding: {
profile: agentProfileName ?? 'agent',
model,
thinking: opts.effort,
strictThinking: opts.effort !== undefined,
},
});
const agent = await ensureMainAgent(session);
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const MIGRATE_CLI_OPTIONS: CLIOptions = {
auto: false,
plan: false,
model: undefined,
effort: undefined,
outputFormat: undefined,
prompt: undefined,
skillsDirs: [],
Expand Down
16 changes: 14 additions & 2 deletions apps/kimi-code/src/tui/controllers/auth-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ export class AuthFlowController {
this.host.setStartupReady();
}

async activateModelAfterLogin(model: string, effort?: string): Promise<void> {
async activateModelAfterLogin(
model: string,
effort?: string,
strictThinking = false,
): Promise<void> {
const { host } = this;
if (host.session !== undefined) {
await host.session.setModel(model);
Expand Down Expand Up @@ -121,6 +125,9 @@ export class AuthFlowController {
options.additionalDirs = [...host.state.appState.additionalDirs];
}
const session = await host.harness.createSession(options);
if (strictThinking && effort !== undefined) {
await session.setThinking(effort);
}
await host.setSession(session);
host.setAppState({
sessionId: session.id,
Expand Down Expand Up @@ -164,7 +171,12 @@ export class AuthFlowController {
return;
}

await this.activateModelAfterLogin(defaultModel, thinkingEffortFromConfig(config.thinking));
const startupEffort = host.options.startup.effort;
await this.activateModelAfterLogin(
defaultModel,
startupEffort ?? thinkingEffortFromConfig(config.thinking),
startupEffort !== undefined,
);
if (host.session === undefined && host.engineV2) {
// Session-less v2: also hydrate permission/plan defaults from the
// refreshed config, same as startup.
Expand Down
66 changes: 46 additions & 20 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
PromptPart,
Session,
SkillSummary,
ThinkingEffort,
WorkspaceTrustInfo,
} from '@moonshot-ai/kimi-code-sdk';
import type { MigrationPlan } from '@moonshot-ai/migration-legacy';
Expand Down Expand Up @@ -404,6 +405,7 @@ export class KimiTUI {
auto: startupInput.cliOptions.auto,
plan: startupInput.cliOptions.plan,
model: startupInput.cliOptions.model,
effort: startupInput.cliOptions.effort,
agentProfile: startupInput.agentProfile,
agentFiles: startupInput.cliOptions.agentFiles,
startupNotice: startupInput.startupNotice,
Expand Down Expand Up @@ -810,10 +812,12 @@ export class KimiTUI {
const { workDir } = this.state.appState;
let session: Session | undefined;
let shouldReplayHistory = false;
let createdFreshSession = false;
const isResumeStartup = startup.sessionFlag !== undefined || startup.continueLast;
const createSessionOptions: MutableCreateSessionOptions = {
workDir,
model: startup.model,
thinking: startup.effort,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the effort override through OAuth login

When a fresh interactive startup reaches auth.login_required, this initial creation is abandoned and AuthFlowController.refreshConfigAfterLogin() later creates the session using thinkingEffortFromConfig(config.thinking) rather than startup.effort. Consequently, kimi --effort low silently loses the requested override whenever the user must log in first; forward the startup effort into the post-login creation path as is already done for the startup model.

Useful? React with 👍 / 👎.

permission: startup.auto ? 'auto' : startup.yolo ? 'yolo' : undefined,
planMode: startup.plan ? true : undefined,
// --agent/--agent-file bind the startup session only; sessions created
Expand Down Expand Up @@ -872,6 +876,7 @@ export class KimiTUI {
shouldReplayHistory = true;
} else {
session = await this.harness.createSession(createSessionOptions);
createdFreshSession = true;
this.startupNotice = combineStartupNotice(
this.startupNotice,
`No sessions to continue under "${workDir}"; starting a fresh session.`,
Expand All @@ -888,12 +893,14 @@ export class KimiTUI {
this.appendStartupNotice(SESSIONLESS_STARTUP_NOTICE);
} else {
session = await this.harness.createSession(createSessionOptions);
createdFreshSession = true;
}
if (session !== undefined && shouldReplayHistory) {
await this.applyStartupModesToResumedSession(session);
if (startup.model !== undefined) {
await session.setModel(startup.model);
}
await this.applyStartupOverridesToResumedSession(session);
} else if (session !== undefined && createdFreshSession && startup.effort !== undefined) {
// createSession intentionally keeps lenient SDK compatibility; an
// explicit CLI effort must use the strict, model-aware setter too.
await session.setThinking(startup.effort);
}
} catch (error) {
if (!isOAuthLoginRequiredError(error)) throw error;
Expand Down Expand Up @@ -1694,18 +1701,23 @@ export class KimiTUI {
if (!startup.plan) {
patch.planMode = config.defaultPlanMode === true;
}
const effort = thinkingEffortFromConfig(config.thinking);
if (effort !== undefined) {
patch.thinkingEffort = effort;
} else if (startupModel !== undefined) {
// No concrete effort configured: mirror the engine, which resolves the
// model's default effort at createSession time.
const raw = config.models?.[startupModel];
if (raw !== undefined) {
const providerType = config.providers?.[raw.provider]?.type;
patch.thinkingEffort = defaultThinkingEffortFor(
effectiveModelAlias(raw, providerType ?? raw.protocol),
);
if (startup.effort !== undefined) {
patch.thinkingEffort = startup.effort as ThinkingEffort;
patch.lazySessionThinking = startup.effort as ThinkingEffort;
} else {
const effort = thinkingEffortFromConfig(config.thinking);
if (effort !== undefined) {
patch.thinkingEffort = effort;
} else if (startupModel !== undefined) {
// No concrete effort configured: mirror the engine, which resolves the
// model's default effort at createSession time.
const raw = config.models?.[startupModel];
if (raw !== undefined) {
const providerType = config.providers?.[raw.provider]?.type;
patch.thinkingEffort = defaultThinkingEffortFor(
effectiveModelAlias(raw, providerType ?? raw.protocol),
);
}
}
}
if (startup.agentProfile !== undefined || startup.agentFiles !== undefined) {
Expand Down Expand Up @@ -1796,6 +1808,11 @@ export class KimiTUI {
let session: Session;
try {
session = await this.createSessionFromCurrentState(true);
if (this.options.startup.effort !== undefined) {
// createSession remains lenient for SDK compatibility. An explicit
// CLI effort must also pass the strict, model-aware session setter.
await session.setThinking(this.options.startup.effort);
}
} catch (error) {
const msg = formatErrorMessage(error);
this.showError(`Failed to start a session: ${msg}`);
Expand Down Expand Up @@ -1856,11 +1873,12 @@ export class KimiTUI {
this.syncAdditionalDirs(session);
}

// Apply --auto/--yolo/--plan startup flags to a resumed session. The resumed
// Apply CLI startup overrides to a resumed session. The resumed
// session may already be in plan mode from its persisted records, and
// re-entering plan mode throws, so only enable it when it is not active yet.
// setPermission is idempotent and needs no such guard.
private async applyStartupModesToResumedSession(session: Session): Promise<void> {
// setPermission is idempotent and needs no such guard. Model must be applied
// before effort because the engine validates effort against the active model.
private async applyStartupOverridesToResumedSession(session: Session): Promise<void> {
const { startup } = this.options;
if (startup.auto) {
await session.setPermission('auto');
Expand All @@ -1873,6 +1891,12 @@ export class KimiTUI {
await session.setPlanMode(true);
}
}
if (startup.model !== undefined) {
await session.setModel(startup.model);
}
if (startup.effort !== undefined) {
await session.setThinking(startup.effort);
}
}

// Re-apply startup flags that the user explicitly passed on the command line.
Expand Down Expand Up @@ -3305,7 +3329,9 @@ export class KimiTUI {
const switched = await this.resumeSession(session.id);
if (!switched) return;
if (applyStartupModes) {
await this.applyStartupModesToResumedSession(this.requireSession());
const resumedSession = this.requireSession();
await this.applyStartupOverridesToResumedSession(resumedSession);
await this.syncRuntimeState(resumedSession);
this.applyStartupPermissionAndPlanToAppState();
}
this.hideSessionPicker();
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ export interface TUIStartupOptions {
readonly auto: boolean;
readonly plan: boolean;
readonly model?: string;
readonly effort?: string;
/** Resolved profile name from --agent/--agent-file; bound to the startup session only. */
readonly agentProfile?: string;
/** Raw --agent-file paths, passed to session creation alongside `agentProfile`. */
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/goal-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ function opts(overrides: Partial<Parameters<typeof runPrompt>[0]> = {}) {
auto: false,
plan: false,
model: undefined,
effort: undefined,
outputFormat: undefined,
prompt: '/goal Ship feature X',
skillsDirs: [],
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ function defaultOpts(): CLIOptions {
auto: false,
plan: false,
model: undefined,
effort: undefined,
outputFormat: undefined,
prompt: undefined,
skillsDirs: [],
Expand Down
Loading