From cb6aef2ffb4b08b69b078938823cf4a3ac1a44f0 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 6 Mar 2026 15:14:53 +0100 Subject: [PATCH] fix(coding-agent): preserve thinking defaults across model switches closes #1864 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 25 ++++-- ...gent-session-model-switch-thinking.test.ts | 79 +++++++++++++++++++ 3 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 packages/coding-agent/test/agent-session-model-switch-thinking.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 33e60d09..632c7c3b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -10,6 +10,7 @@ - Fixed `/new` leaving startup header content, including the changelog, visible after starting a fresh session ([#1880](https://github.com/badlogic/pi-mono/issues/1880)) - Fixed misleading docs and example implying that returning `{ isError: true }` from a tool's `execute` function marks the execution as failed; errors must be signaled by throwing ([#1881](https://github.com/badlogic/pi-mono/issues/1881)) +- Fixed model switches through non-reasoning models to preserve the saved default thinking level instead of persisting a capability-forced `off` clamp ([#1864](https://github.com/badlogic/pi-mono/issues/1864)) ## [0.56.2] - 2026-03-05 diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index ea544e18..610f4885 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1315,12 +1315,13 @@ export class AgentSession { } const previousModel = this.model; + const thinkingLevel = this._getThinkingLevelForModelSwitch(); this.agent.setModel(model); this.sessionManager.appendModelChange(model.provider, model.id); this.settingsManager.setDefaultModelAndProvider(model.provider, model.id); // Re-clamp thinking level for new model's capabilities - this.setThinkingLevel(this.thinkingLevel); + this.setThinkingLevel(thinkingLevel); await this._emitModelSelect(model, previousModel, "set"); } @@ -1371,6 +1372,7 @@ export class AgentSession { const len = scopedModels.length; const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len; const next = scopedModels[nextIndex]; + const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel); // Apply model this.agent.setModel(next.model); @@ -1379,9 +1381,9 @@ export class AgentSession { // Apply thinking level. // - Explicit scoped model thinking level overrides current session level - // - Undefined scoped model thinking level inherits current session level + // - Undefined scoped model thinking level inherits the current session preference // setThinkingLevel clamps to model capabilities. - this.setThinkingLevel(next.thinkingLevel ?? this.thinkingLevel); + this.setThinkingLevel(thinkingLevel); await this._emitModelSelect(next.model, currentModel, "cycle"); @@ -1405,12 +1407,13 @@ export class AgentSession { throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`); } + const thinkingLevel = this._getThinkingLevelForModelSwitch(); this.agent.setModel(nextModel); this.sessionManager.appendModelChange(nextModel.provider, nextModel.id); this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id); // Re-clamp thinking level for new model's capabilities - this.setThinkingLevel(this.thinkingLevel); + this.setThinkingLevel(thinkingLevel); await this._emitModelSelect(nextModel, currentModel, "cycle"); @@ -1437,7 +1440,9 @@ export class AgentSession { if (isChanging) { this.sessionManager.appendThinkingLevelChange(effectiveLevel); - this.settingsManager.setDefaultThinkingLevel(effectiveLevel); + if (this.supportsThinking() || effectiveLevel !== "off") { + this.settingsManager.setDefaultThinkingLevel(effectiveLevel); + } } } @@ -1480,6 +1485,16 @@ export class AgentSession { return !!this.model?.reasoning; } + private _getThinkingLevelForModelSwitch(explicitLevel?: ThinkingLevel): ThinkingLevel { + if (explicitLevel !== undefined) { + return explicitLevel; + } + if (!this.supportsThinking()) { + return this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL; + } + return this.thinkingLevel; + } + private _clampThinkingLevel(level: ThinkingLevel, availableLevels: ThinkingLevel[]): ThinkingLevel { const ordered = THINKING_LEVELS_WITH_XHIGH; const available = new Set(availableLevels); diff --git a/packages/coding-agent/test/agent-session-model-switch-thinking.test.ts b/packages/coding-agent/test/agent-session-model-switch-thinking.test.ts new file mode 100644 index 00000000..f77785dc --- /dev/null +++ b/packages/coding-agent/test/agent-session-model-switch-thinking.test.ts @@ -0,0 +1,79 @@ +import { Agent, type ThinkingLevel } from "@mariozechner/pi-agent-core"; +import { getModel } from "@mariozechner/pi-ai"; +import { describe, expect, it } from "vitest"; +import { AgentSession } from "../src/core/agent-session.js"; +import { AuthStorage } from "../src/core/auth-storage.js"; +import { ModelRegistry } from "../src/core/model-registry.js"; +import { SessionManager } from "../src/core/session-manager.js"; +import { SettingsManager } from "../src/core/settings-manager.js"; +import { createTestResourceLoader } from "./utilities.js"; + +const reasoningModel = getModel("anthropic", "claude-sonnet-4-5")!; +const nonReasoningModel = getModel("anthropic", "claude-3-5-haiku-latest")!; + +function createSession({ + thinkingLevel = "high", + defaultThinkingLevel = thinkingLevel, + scopedModels, +}: { + thinkingLevel?: ThinkingLevel; + defaultThinkingLevel?: ThinkingLevel; + scopedModels?: Array<{ model: typeof reasoningModel; thinkingLevel?: ThinkingLevel }>; +} = {}) { + const settingsManager = SettingsManager.inMemory({ defaultThinkingLevel }); + const sessionManager = SessionManager.inMemory(); + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey("anthropic", "test-key"); + const session = new AgentSession({ + agent: new Agent({ + getApiKey: () => "test-key", + initialState: { + model: reasoningModel, + systemPrompt: "You are a helpful assistant.", + tools: [], + thinkingLevel, + }, + }), + sessionManager, + settingsManager, + cwd: process.cwd(), + modelRegistry: new ModelRegistry(authStorage, undefined), + resourceLoader: createTestResourceLoader(), + scopedModels, + }); + + return { session, sessionManager, settingsManager }; +} + +describe("AgentSession model switching", () => { + it("preserves the saved thinking preference through non-reasoning models", async () => { + const { session, sessionManager, settingsManager } = createSession({ + scopedModels: [{ model: reasoningModel }, { model: nonReasoningModel }], + }); + + try { + await session.setModel(nonReasoningModel); + expect(session.thinkingLevel).toBe("off"); + expect(settingsManager.getDefaultThinkingLevel()).toBe("high"); + + await session.setModel(reasoningModel); + expect(session.thinkingLevel).toBe("high"); + + await session.cycleModel(); + expect(session.thinkingLevel).toBe("off"); + expect(settingsManager.getDefaultThinkingLevel()).toBe("high"); + + await session.cycleModel(); + expect(session.thinkingLevel).toBe("high"); + expect(settingsManager.getDefaultThinkingLevel()).toBe("high"); + expect( + sessionManager + .getEntries() + .filter((entry) => entry.type === "thinking_level_change") + .map((entry) => entry.thinkingLevel), + ).toEqual(["off", "high", "off", "high"]); + } finally { + session.dispose(); + } + }); +});