diff --git a/packages/agent-runtime/src/pi/bridge/bridge.ts b/packages/agent-runtime/src/pi/bridge/bridge.ts index cdac5438d..667774fa4 100644 --- a/packages/agent-runtime/src/pi/bridge/bridge.ts +++ b/packages/agent-runtime/src/pi/bridge/bridge.ts @@ -50,6 +50,7 @@ import type { ImageContent } from "@earendil-works/pi-ai"; import { createPiDeltaTranslator } from "../delta-translation.js"; import { buildPiSessionParams, + toPiThinkingLevel, type PiSessionParams, } from "../session-params.js"; import { PiSdkSession, type PiSdkSessionOptions } from "./sdk-session.js"; @@ -971,6 +972,20 @@ async function handleTurnStart( return; } + // Execution options ride every turn command and the runtime never diffs + // them (#2160): a model or reasoning level picked after the session was + // constructed is applied to the live session here, before dispatch. + try { + await threadSession.session.applyTurnOptions({ + model: params.options.model, + thinkingLevel: toPiThinkingLevel(params.options.reasoningLevel), + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + sendError(id, -32000, message); + return; + } + // A standalone builtin `/compact` mention is bb's manual-compaction request, // not model input. Prompting with the literal text would make the model talk // about compaction while the context keeps growing. diff --git a/packages/agent-runtime/src/pi/bridge/sdk-session.ts b/packages/agent-runtime/src/pi/bridge/sdk-session.ts index df3d8ad07..b1b79ad17 100644 --- a/packages/agent-runtime/src/pi/bridge/sdk-session.ts +++ b/packages/agent-runtime/src/pi/bridge/sdk-session.ts @@ -435,6 +435,49 @@ export class PiSdkSession { this.monitorSteerConsumption(tracked.promise); } + /** + * Reconcile the execution options a turn command carries with the live + * session. Options ride every command (the runtime never diffs them), so a + * model or thinking level the user changed after construction is applied + * here, before the input is dispatched. Unchanged values are a no-op. + */ + async applyTurnOptions(args: { + model: string | undefined; + thinkingLevel: CreateAgentSessionOptions["thinkingLevel"] | undefined; + }): Promise { + if (!this.session) { + throw new Error("No active Pi SDK session"); + } + if (args.model !== undefined) { + const next = resolveConfiguredModel(this.session.modelRuntime, args.model); + const current = this.session.model; + if ( + next && + (current === undefined || + current.provider !== next.provider || + current.id !== next.id) + ) { + this.recordSdkBoundary("bridge→provider", { + method: "setModel", + params: { provider: next.provider, id: next.id }, + }); + await this.session.setModel(next); + this.options.model = args.model; + } + } + if ( + args.thinkingLevel !== undefined && + args.thinkingLevel !== this.session.thinkingLevel + ) { + this.recordSdkBoundary("bridge→provider", { + method: "setThinkingLevel", + params: { level: args.thinkingLevel }, + }); + this.session.setThinkingLevel(args.thinkingLevel); + this.options.thinkingLevel = args.thinkingLevel; + } + } + async compact(): Promise { if (!this.session) { throw new Error("No active Pi SDK session"); diff --git a/packages/agent-runtime/src/pi/session-params.ts b/packages/agent-runtime/src/pi/session-params.ts index d0e07b03e..fa0fef9ca 100644 --- a/packages/agent-runtime/src/pi/session-params.ts +++ b/packages/agent-runtime/src/pi/session-params.ts @@ -13,7 +13,7 @@ type PiReasoningLevel = "off" | "low" | "medium" | "high" | "xhigh" | "max"; // Levels Pi does not support ("ultracode", "ultra") are dropped so the bridge // never receives a value it would reject; reconciliation picks the closest // supported level before this point, so this is a defensive floor. -function toPiThinkingLevel( +export function toPiThinkingLevel( reasoningLevel: ReasoningLevel | undefined, ): PiReasoningLevel | undefined { switch (reasoningLevel) {