Skip to content

Apply turn options to the live Pi session - #2221

Open
SawyerHood wants to merge 1 commit into
mainfrom
fix/2160-pi-turn-model-options
Open

Apply turn options to the live Pi session#2221
SawyerHood wants to merge 1 commit into
mainfrom
fix/2160-pi-turn-model-options

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

What was wrong

In a Pi thread, picking a different model (or reasoning level) in the composer changed what bb recorded on client/turn/requested but not what Pi used. The Pi bridge read options.model / options.reasoningLevel only in the session-construction mapping (thread/start, thread/resume, thread/fork); handleTurnStart and handleTurnSteer ignored params.options entirely. Since #1640 the runtime no longer diffs execution options and classifies every change as "live" (it rides the next turn command and the bridge is expected to reconcile), so nothing ever called into Pi with the new model and nothing rebuilt the session. Every later turn, and any manual /compact (which is just another turn/start), kept running on the construction model until a session rebuild (bb thread stop, daemon restart, bridge recovery) happened to occur. Codex and Claude bridges got the per-turn reconciliation in #1640; Pi did not. Report: https://get-bb.github.io/reports/issues/2160.html

What changed

  • packages/agent-runtime/src/pi/bridge/sdk-session.ts: new PiSdkSession.applyTurnOptions({ model, thinkingLevel }). It resolves the model with the existing resolveConfiguredModel semantics (provider prefix authoritative, ambiguous bare ids rejected), auth-checks it through modelRuntime.checkAuth, swaps agent.state.model, appends model_change to the session file, then re-clamps the thinking level to the (possibly new) model with clampThinkingLevel and appends thinking_level_change when it changed. Unchanged values are a no-op. It throws when the model cannot be resolved or has no credentials.
    • Deviation from the report's prototype, per its own caveat: it does not call AgentSession.setModel / setThinkingLevel. Both persist the selection as the user's default into the global ~/.pi/agent/settings.json (setDefaultModelAndProvider / setDefaultThinkingLevel), which session construction never does; a per-thread bb pick must not rewrite the user's pi CLI defaults. The lower-level SDK pieces are used instead, same as the SDK's own construction path. The one thing this skips relative to setModel is the model_select extension hook (the extension runner is private on AgentSession).
  • packages/agent-runtime/src/pi/bridge/bridge.ts: applyTurnOptionsOrFail runs at the top of handleTurnStart (before both the /compact and prompt branches, so the summarization request also goes to the selected model) and in handleTurnSteer. A failed resolution fails the turn with the error message (-32000) instead of silently keeping the old model.
  • packages/agent-runtime/src/pi/session-params.ts: buildPiTurnOptions maps canonical execution options to the turn-applicable subset, reusing the same toPiThinkingLevel floor construction uses.

No wire change between server and host daemon (the fix is entirely inside the Pi bridge), so no HOST_DAEMON_PROTOCOL_VERSION bump. No CLI/knob changes. I did not add a generic conformance rule: the conformance suite observes only wire messages and cannot tell which model a provider actually ran a turn on without a provider-specific probe, so the guard lives in the Pi bridge tests.

How you verified

Tests added (all 8 fail on main with the test changes alone, pass with the fix):

  • packages/agent-runtime/src/pi/bridge/__tests__/bridge.test.ts: drives the real bridge through the JSON-RPC harness with a model-tracking Pi session stand-in. turn/start with a different model + reasoning level than thread/start prompts on the new model/level without rebuilding; a /compact turn carrying a new model compacts on it; turn/steer applies the model before steering; an unresolvable model fails the turn with Failed to resolve Pi model "unsupported/model" and leaves the session's model untouched.
  • packages/agent-runtime/src/pi/bridge/__tests__/sdk-session.test.ts: applyTurnOptions switches model + level and appends to the session file while setModel, setThinkingLevel, settingsManager.setDefaultModelAndProvider, and settingsManager.setDefaultThinkingLevel are never called; re-clamps the level to off when the new model has no reasoning; is a no-op for unchanged options; rejects a model whose provider has no credentials and keeps the current model.

Commands: pnpm exec turbo run typecheck test --filter=@bb/agent-runtime (green, 11 Pi suites + the new tests), prettier on touched files.

Live, against my own dev instance of this branch with the real Pi SDK (github-copilot models): spawned a Pi thread on github-copilot/gpt-5-mini --reasoning-level low ("Reply only with ok."), then bb thread tell --model github-copilot/grok-4.6 --reasoning-level high. The Pi bridge session file shows the second assistant message served by github-copilot/grok-4.6 with a model_change + thinking_level_change: high entry appended before it, and the bb event log shows no thread/identity between the turns (no rebuild):

model_change  github-copilot/gpt-5-mini
assistant     github-copilot/gpt-5-mini   in=5318 out=36
model_change  github-copilot/grok-4.6
assistant     github-copilot/grok-4.6     in=5939 out=12

~/.pi/agent/settings.json was byte-identical (md5) before and after, so the user's Pi defaults were not rewritten.

Fixes #2160

AGENT GENERATED: by Claude Opus 5

The Pi bridge read options.model and options.reasoningLevel only when it
constructed a session (thread/start, resume, fork). handleTurnStart and
handleTurnSteer ignored params.options, so a model or reasoning level
picked after the session existed never reached Pi: every later turn, and
any manual compaction, ran on the construction model until something
rebuilt the session. The runtime stopped diffing execution options in
#1640 and expects each bridge to reconcile them itself; Codex and Claude
do, Pi did not.

Reconcile on every turn/start and turn/steer before dispatch, ahead of
both the prompt and /compact branches. The SDK's setModel and
setThinkingLevel are deliberately not used: both persist the selection as
the user's default in ~/.pi/agent/settings.json, which construction never
does. The bridge applies the lower-level pieces instead (auth check, agent
state, session-file model_change/thinking_level_change, thinking-level
re-clamp). A model that does not resolve or has no credentials fails the
turn with that error instead of silently keeping the old model.

Fixes #2160

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pi keeps using previous model after model picker change until /compact

2 participants