fix(coding-agent): preserve thinking defaults across model switches closes #1864
This commit is contained in:
@@ -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 `/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 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
|
## [0.56.2] - 2026-03-05
|
||||||
|
|
||||||
|
|||||||
@@ -1315,12 +1315,13 @@ export class AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previousModel = this.model;
|
const previousModel = this.model;
|
||||||
|
const thinkingLevel = this._getThinkingLevelForModelSwitch();
|
||||||
this.agent.setModel(model);
|
this.agent.setModel(model);
|
||||||
this.sessionManager.appendModelChange(model.provider, model.id);
|
this.sessionManager.appendModelChange(model.provider, model.id);
|
||||||
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
||||||
|
|
||||||
// Re-clamp thinking level for new model's capabilities
|
// Re-clamp thinking level for new model's capabilities
|
||||||
this.setThinkingLevel(this.thinkingLevel);
|
this.setThinkingLevel(thinkingLevel);
|
||||||
|
|
||||||
await this._emitModelSelect(model, previousModel, "set");
|
await this._emitModelSelect(model, previousModel, "set");
|
||||||
}
|
}
|
||||||
@@ -1371,6 +1372,7 @@ export class AgentSession {
|
|||||||
const len = scopedModels.length;
|
const len = scopedModels.length;
|
||||||
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
||||||
const next = scopedModels[nextIndex];
|
const next = scopedModels[nextIndex];
|
||||||
|
const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
|
||||||
|
|
||||||
// Apply model
|
// Apply model
|
||||||
this.agent.setModel(next.model);
|
this.agent.setModel(next.model);
|
||||||
@@ -1379,9 +1381,9 @@ export class AgentSession {
|
|||||||
|
|
||||||
// Apply thinking level.
|
// Apply thinking level.
|
||||||
// - Explicit scoped model thinking level overrides current session 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.
|
// setThinkingLevel clamps to model capabilities.
|
||||||
this.setThinkingLevel(next.thinkingLevel ?? this.thinkingLevel);
|
this.setThinkingLevel(thinkingLevel);
|
||||||
|
|
||||||
await this._emitModelSelect(next.model, currentModel, "cycle");
|
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}`);
|
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const thinkingLevel = this._getThinkingLevelForModelSwitch();
|
||||||
this.agent.setModel(nextModel);
|
this.agent.setModel(nextModel);
|
||||||
this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
|
this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
|
||||||
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
||||||
|
|
||||||
// Re-clamp thinking level for new model's capabilities
|
// Re-clamp thinking level for new model's capabilities
|
||||||
this.setThinkingLevel(this.thinkingLevel);
|
this.setThinkingLevel(thinkingLevel);
|
||||||
|
|
||||||
await this._emitModelSelect(nextModel, currentModel, "cycle");
|
await this._emitModelSelect(nextModel, currentModel, "cycle");
|
||||||
|
|
||||||
@@ -1437,7 +1440,9 @@ export class AgentSession {
|
|||||||
|
|
||||||
if (isChanging) {
|
if (isChanging) {
|
||||||
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
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;
|
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 {
|
private _clampThinkingLevel(level: ThinkingLevel, availableLevels: ThinkingLevel[]): ThinkingLevel {
|
||||||
const ordered = THINKING_LEVELS_WITH_XHIGH;
|
const ordered = THINKING_LEVELS_WITH_XHIGH;
|
||||||
const available = new Set(availableLevels);
|
const available = new Set(availableLevels);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user