模型切换与设置热更新怎样保持一致
追踪 setModel、cycleModel、setThinkingLevel、启动恢复与 reload,区分当前 Agent 状态、会话分支记录和跨会话默认值的所有权。
Pi 同时保存三种“当前模型”。Agent.state.model 决定下一次请求用谁;session 树里的 model_change 说明这条分支当时选择了谁;全局 settings.json 的 default provider/model 决定以后新建会话从哪里起步。三者经常相同,但所有权不同。
flowchart TD
accTitle: 模型切换的三层状态
accDescr: setModel 校验认证后,先更新当前 Agent,再追加 session change,并更新跨会话默认;resume 优先从 session 恢复,新 session 才主要依赖默认值
SM["setModel(next)"] --> AU["ModelRuntime.checkAuth"]
AU --> AS["Agent.state.model\n下一次请求"]
AS --> SE["SessionManager.appendModelChange\n当前分支历史"]
SE --> GS["SettingsManager default\n以后新会话"]
GS --> TL["按 next model clamp thinking"]
TL --> EV["model_select event"]
RES["resume"] --> SE
NEW["new session"] --> GS
setModel() 先拒绝不可用选择
模型对象出现在目录里不代表当前运行时有认证材料。setModel() 首先调用 ModelRuntime.checkAuth(provider),没有结果就抛错,不修改任何状态。通过后,它保存 previous model,写入 Agent.state.model,追加 model_change,更新 settings 中的默认 provider/model;随后按新模型能力重设 thinking level,最后异步发出 model_select(source: "set")。
这条路径不是事务。SessionManager.appendModelChange() 是同步 append;SettingsManager.setDefaultModelAndProvider() 立即更新内存合并结果,但文件写入进入内部 Promise queue。extension event 发出时,settings 写入可能尚未 flush。进程的正常收尾可 await settingsManager.flush(),但监听到 model_select 不能当作 settings 文件已经耐久落盘的证明。
Cycle 先缩小候选集,再走相同写入
cycleModel() 有两套候选集合。命令行 --models 提供 scoped models 时,它先并发检查每个 provider 的认证,只在可用项中循环,并允许某个 scoped model 带显式 thinking level;否则从 ModelRuntime.getAvailable() 的全量可用模型中循环。两条路径最后都更新 Agent、session 和 settings,再触发 model_select(source: "cycle")。
当候选只有一个时,cycle 返回 undefined,不是一次相同模型的“成功切换”。若当前 model 不在候选表里,代码从 index 0 作为基准再按方向移动;这在动态 provider 或配置刚变化时会影响第一步落点。
Thinking level 是偏好,不是绕过模型能力的开关
setThinkingLevel() 先取得当前模型可用级别,不支持传入值时调用 clampThinkingLevel()。只有 effective level 与旧值不同,才追加 thinking_level_change、更新默认 thinking level 并发出本地与扩展事件。对不支持 reasoning 的模型,high 最终可能变成 off。
这里还保留一项细节:如果当前模型不支持 thinking 且 effective level 为 off,代码不会用这个结果覆盖用户全局的 thinking 偏好。这样切回 reasoning model 时,_getThinkingLevelForModelSwitch() 可以从 settings 恢复默认,而不是永远继承 off。
Resume 读 session,新会话才回落到默认值
新 runtime 创建时,SDK 先用 buildSessionContext() 查当前分支的 model 与 thinking level。既有 session 的 model 能在目录中找到且 provider 已配置认证时,它优先于 settings;恢复失败会产生 fallback message,再调用 findInitialModel()。thinking level 有显式 change entry 时从 session 恢复,没有时才取 settings 默认,并最终按选中的模型 clamp。
初始化 Agent 后,既有 session 的投影消息被写入 Agent;若老会话没有 thinking entry,SDK 补写一条。全新 session 则立即追加初始 model change 与 thinking change。因而默认值是初始化输入,session entry 才是之后 resume 的分支事实。
Reload 重建扩展 runtime,不等于 session replacement
AgentSession.reload() 等待旧 extension 的 shutdown,重新读取 settings,同步 steering/follow-up 模式,重置 API provider 全局注册,reload 资源,再以当前 active tool names 重建 ExtensionRunner 与工具表。它仍在同一个 AgentSession 对象上工作,保留当前 Agent、SessionManager、ModelRuntime 引用和消息状态。
依赖存在时,仓库的 faux model 测试可以验证切换、事件和 capability clamp:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
cd "$repo"
npx vitest --run packages/coding-agent/test/suite/agent-session-model-extension.test.ts
# 无依赖时静态检查三层写入顺序
git show v0.83.0:packages/coding-agent/src/core/agent-session.ts |
nl -ba | sed -n '1573,1745p;2602,2625p'
这次没有为固定 checkout 安装依赖,faux provider 命令只作为复现入口。模型与 thinking 的当前值、分支记录和默认值现在有了清楚归属;第五部最后一章回到运行结束后的控制逻辑,解释 retry、compaction 与 usage 为什么共享部分数据,却不能合并成一个计数器。