AgentSession — Model Management(模型管理)

文件位置:F:\Pi\packages\coding-agent\src\core\agent-session.ts

依赖的配置类型:F:\Pi\packages\coding-agent\src\core\settings-manager.tsdefaultThinkingLevelsetDefaultModelAndProvider()

模型注册表:F:\Pi\packages\coding-agent\src\core\model-registry.tsModelRegistry

AI 兼容层:@earendil-works/pi-ai/compatclampThinkingLevel()getSupportedThinkingLevels()modelsAreEqual()

默认值:F:\Pi\packages\coding-agent\src\core\defaults.tsDEFAULT_THINKING_LEVEL = "medium"

编写目的:详细拆解 AgentSession 中 Model Management(模型管理)机制的完整实现,涵盖模型选择/切换、thinking level 管理、scoped models 等核心逻辑。


一、Model Management 概述

Model Management 是 AgentSession 中负责管理当前 LLM 模型及相关配置的模块。它包括:

  1. 模型选择与切换setModel() 直接设置、cycleModel() 循环切换
  2. Thinking Level 管理setThinkingLevel()cycleThinkingLevel()supportsThinking()
  3. Scoped Models — 通过 --models 标志限定的可用模型列表
  4. 模型切换事件model_select 扩展事件

核心数据结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Thinking Level 枚举值
const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"];

// 默认 Thinking Level
const DEFAULT_THINKING_LEVEL: ThinkingLevel = "medium";

// cycleModel() 返回类型
export interface ModelCycleResult {
  model: Model<any>;        // 切换后的模型
  thinkingLevel: ThinkingLevel;  // 切换后的 thinking level
  isScoped: boolean;        // 是否来自 scoped models
}

完整流程全景

用户操作
    │
    ├─ Ctrl+P (cycleModel)
    │     │
    │     ├─ 有 scoped models ──→ _cycleScopedModel()
    │     │                         ├─ 过滤可用的 scoped model
    │     │                         ├─ 计算下一个索引(forward/backward)
    │     │                         ├─ 应用 thinking level(scoped 自带优先)
    │     │                         └─ 更新 agent.state + session + settings
    │     │
    │     └─ 无 scoped models ──→ _cycleAvailableModel()
    │                              ├─ 从 ModelRegistry 获取所有可用模型
    │                              ├─ 计算下一个索引
    │                              └─ 更新 agent.state + session + settings
    │
    ├─ /model 命令 (setModel)
    │     └─ 直接设置模型 + 验证 auth
    │
    └─ Ctrl+T (cycleThinkingLevel)
          └─ 模型支持 thinking? → 循环下一个 level

状态持久化

每次模型或 thinking level 变更,都会同时写入三处:

存储位置写入方式用途
agent.state.model / agent.state.thinkingLevel直接赋值当前会话的运行时状态
sessionManager.appendModelChange() / appendThinkingLevelChange()记录为 session entry会话文件持久化,支持 undo/redo 和分支导航时恢复
settingsManager.setDefaultModelAndProvider() / setDefaultThinkingLevel()写入 settings.json下次启动时恢复为默认值

二、模型选择与切换

2.1 setModel(model) — 直接设置模型

1
async setModel(model: Model<any>): Promise<void>

位置agent-session.ts 第 1453 行

功能:直接设置当前 LLM 模型。会验证认证配置,保存到会话和设置中,并重新钳制 thinking level。

执行流程

setModel(model)
  │
  ├─ hasConfiguredAuth(model) === false → 抛出 "No API key for {provider}/{id}"
  │
  ├─ 保存 previousModel
  │
  ├─ 决定 thinking level(_getThinkingLevelForModelSwitch)
  │
  ├─ agent.state.model = model
  │
  ├─ sessionManager.appendModelChange(provider, id)
  │
  ├─ settingsManager.setDefaultModelAndProvider(provider, id)
  │
  ├─ setThinkingLevel(thinkingLevel)   // 重新钳制以适应新模型能力
  │
  └─ _emitModelSelect(model, previousModel, "set")

参数说明

参数类型说明
modelModel<any>要设置的目标模型对象,包含 provider、id、contextWindow、reasoning 等属性

认证校验ModelRegistry.hasConfiguredAuth() 检查策略如下:

1
2
3
4
5
6
7
8
hasConfiguredAuth(model: Model<Api>): boolean {
  const providerApiKey = this.providerRequestConfigs.get(model.provider)?.apiKey;
  return (
    this.authStorage.hasAuth(model.provider) ||        // OAuth / 已存储的 key
    (providerApiKey !== undefined &&                    // 配置中的 API key
     isConfigValueConfigured(providerApiKey))
  );
}

使用场景

  • /model 命令的回调
  • /reload 后恢复模型
  • Extension 调用 pi.setModel()(通过 ExtensionAPI)
  • 会话恢复时从 session header 还原模型

2.2 cycleModel(direction) — 循环切换模型

1
2
3
async cycleModel(
  direction: "forward" | "backward" = "forward"
): Promise<ModelCycleResult | undefined>

位置agent-session.ts 第 1476 行

功能:在可用模型列表中循环切换到下一个/上一个模型。优先使用 scoped models(--models 标志指定的范围),其次使用全部可用模型。

决策逻辑

cycleModel(direction)
  │
  ├─ scopedModels.length > 0 ──→ _cycleScopedModel(direction)
  │
  └─ scopedModels.length === 0 ──→ _cycleAvailableModel(direction)

返回值

  • ModelCycleResult — 切换成功,包含新模型、thinking level、是否来自 scoped
  • undefined — 只有一个可用模型,无需切换

2.3 _cycleScopedModel(direction) — Scoped 模型循环

位置agent-session.ts 第 1483 行

功能:在 --models 标志指定的 scoped models 列表中循环切换。

执行流程

_cycleScopedModel(direction)
  │
  ├─ 过滤出已配置认证的 scoped models
  ├─ scopedModels.length <= 1 → return undefined
  │
  ├─ 查找当前模型在 scoped 列表中的索引
  ├─ 计算下一个索引:
  │   forward:  (currentIndex + 1) % len
  │   backward: (currentIndex - 1 + len) % len
  │
  ├─ 决定 thinking level:
  │   ├─ scoped model 有显式 thinkingLevel → 使用它(覆盖当前 session 的 level)
  │   └─ scoped model 无显式 thinkingLevel → 继承当前 session 的 level
  │
  ├─ agent.state.model = next.model
  ├─ sessionManager.appendModelChange(next.model.provider, next.model.id)
  ├─ settingsManager.setDefaultModelAndProvider(...)
  ├─ setThinkingLevel(thinkingLevel)  // 钳制到新模型能力
  │
  └─ return { model, thinkingLevel, isScoped: true }

设计意图:scoped models 允许用户通过 --models anthropic:claude-sonnet-4 anthropic:claude-haiku-3 限定可切换的模型范围,避免在几十个模型中循环。每个 scoped model 还可以自带 thinking level,实现"切换到 Claude Sonnet 时自动设为 high,切换到 Haiku 时设为 off"的快捷切换。


2.4 _cycleAvailableModel(direction) — 全部模型循环

位置agent-session.ts 第 1523 行

功能:在 ModelRegistry 中所有已配置认证的模型中循环切换。

执行流程

_cycleAvailableModel(direction)
  │
  ├─ modelRegistry.getAvailable() → 所有已配置认证的模型
  ├─ availableModels.length <= 1 → return undefined
  │
  ├─ 查找当前模型索引
  ├─ 计算下一个索引(同 scoped 算法)
  ├─ 应用模型 + 持久化(同 scoped)
  ├─ thinking level 继承当前 session 的 level
  │
  └─ return { model, thinkingLevel, isScoped: false }

与 Scoped 的关键区别

维度_cycleScopedModel_cycleAvailableModel
模型来源用户 --models 标志指定的列表ModelRegistry 中所有可用模型
数量级通常 2-5 个可能 20+ 个(所有 Provider 的所有模型)
Thinking Level可跟随 scoped model 的显式设置始终继承当前 session 的 level
过滤条件已配置认证已配置认证

2.5 _emitModelSelect(next, previous, source) — 模型切换事件

1
2
3
4
5
private async _emitModelSelect(
  nextModel: Model<any>,
  previousModel: Model<any> | undefined,
  source: "set" | "cycle" | "restore"
): Promise<void>

位置agent-session.ts 第 1434 行

功能:向 extension runner 发送 model_select 事件。如果前后模型相同(通过 modelsAreEqual() 判断),则跳过发送。

事件结构(定义在 ts-src/types.ts):

1
2
3
4
5
6
export interface ModelSelectEvent {
  type: "model_select";
  model: Model<any>;
  previousModel: Model<any> | undefined;
  source: "set" | "cycle" | "restore";
}

source 取值含义

source触发场景
"set"setModel() 直接设置
"cycle"cycleModel() 循环切换
"restore"会话恢复时还原模型

设计意图:Extension 可以通过监听此事件实现模型切换时的副作用——如更新 UI 中的模型指示器、记录审计日志、调用外部 API 同步模型配置等。


三、Thinking Level 管理

3.1 setThinkingLevel(level) — 设置 Thinking Level

1
setThinkingLevel(level: ThinkingLevel): void

位置agent-session.ts 第 1564 行

功能:设置当前模型的 thinking/reasoning level。会钳制到模型支持的范围,仅在实际发生变化时持久化。

完整流程

setThinkingLevel(level)
  │
  ├─ 获取当前模型的可用 thinking levels
  ├─ level 可用?──是──→ effectiveLevel = level
  │                 否
  │                  └─→ effectiveLevel = _clampThinkingLevel(level, available)
  │
  ├─ effectiveLevel === previousLevel?──是──→ 跳过持久化(无变化)
  │
  ├─ agent.state.thinkingLevel = effectiveLevel
  ├─ sessionManager.appendThinkingLevelChange(effectiveLevel)
  ├─ settingsManager.setDefaultThinkingLevel(effectiveLevel)
  │   (仅当模型支持 thinking 或 level !== "off" 时保存)
  │
  ├─ 发出 thinking_level_changed 事件 → UI 更新
  └─ 发出 thinking_level_select 事件 → Extension 通知

可用 Thinking Levels

1
["off", "minimal", "low", "medium", "high"]

每个模型通过 getSupportedThinkingLevels(model) 报告其支持哪些 level。不支持的 level 会被 clampThinkingLevel() 自动调整到最近的有效值。

设计意图:不同模型对 thinking 的支持不同——Claude Sonnet 支持所有 5 个 level,某些低成本模型可能只支持 offoff + lowclampThinkingLevel() 确保用户无法设置模型不支持的 level,同时不抛出错误。


3.2 cycleThinkingLevel() — 循环 Thinking Level

1
cycleThinkingLevel(): ThinkingLevel | undefined

位置agent-session.ts 第 1593 行

功能:在当前模型的可用 thinking levels 中循环到下一个。如果模型不支持 thinking,返回 undefined

cycleThinkingLevel()
  │
  ├─ !supportsThinking() → return undefined
  │
  ├─ levels = getAvailableThinkingLevels()
  ├─ currentIndex = levels.indexOf(currentLevel)
  ├─ nextIndex = (currentIndex + 1) % levels.length
  ├─ setThinkingLevel(nextLevel)
  │
  └─ return nextLevel

UI 绑定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// interactive-mode.ts
this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());

private cycleThinkingLevel(): void {
  const newLevel = this.session.cycleThinkingLevel();
  if (newLevel === undefined) {
    this.showStatus("Current model does not support thinking");
  } else {
    this.showStatus(`Thinking level: ${newLevel}`);
  }
}

3.3 supportsThinking() — 检查 Thinking 能力

1
supportsThinking(): boolean

位置agent-session.ts 第 1607 行

1
2
3
supportsThinking(): boolean {
  return !!this.model?.reasoning;
}

模型对象中的 reasoning 字段标记该模型是否支持 extended thinking/reasoning 功能。


3.4 getAvailableThinkingLevels() — 获取可用 Levels

1
getAvailableThinkingLevels(): ThinkingLevel[]

位置agent-session.ts 第 1601 行

1
2
3
4
getAvailableThinkingLevels(): ThinkingLevel[] {
  if (!this.model) return THINKING_LEVELS;
  return getSupportedThinkingLevels(this.model) as ThinkingLevel[];
}

无模型时返回全部 5 个 level 作为保底值。


3.5 _getThinkingLevelForModelSwitch(explicitLevel?) — 切换时的 Level 决策

1
2
3
private _getThinkingLevelForModelSwitch(
  explicitLevel?: ThinkingLevel
): ThinkingLevel

位置agent-session.ts 第 1610 行

功能:在模型切换时决定应该使用什么 thinking level。这是 Model Management 中最关键的决策点。

决策逻辑

_getThinkingLevelForModelSwitch(explicitLevel?)
  │
  ├─ explicitLevel !== undefined
  │   → 使用 scoped model 自带的显式 level(覆盖当前 session)
  │
  ├─ !supportsThinking() (新模型不支持 thinking)
  │   → 使用 settings 中的默认值(可能为 undefined)
  │   → 如果 undefined,回退到 DEFAULT_THINKING_LEVEL = "medium"
  │
  └─ 新模型支持 thinking
      → 保持当前 session 的 thinking level 不变

三种场景

场景输入输出原因
Scoped model 自带 levelexplicitLevel = "high""high"用户通过 --models 指定了偏好
新模型不支持 thinkingexplicitLevel = undefinedsettings.default ?? "medium"从 settings 恢复用户偏好
同级别切换(都支持 thinking)explicitLevel = undefinedthis.thinkingLevel当前 level 在新模型上应保持一致

3.6 _clampThinkingLevel(level, available) — 钳制到有效范围

1
2
3
4
private _clampThinkingLevel(
  level: ThinkingLevel,
  _availableLevels: ThinkingLevel[]
): ThinkingLevel

位置agent-session.ts 第 1620 行

1
2
3
4
5
private _clampThinkingLevel(level, _availableLevels): ThinkingLevel {
  return this.model
    ? (clampThinkingLevel(this.model, level) as ThinkingLevel)
    : "off";
}

委托给 AI 层的 clampThinkingLevel() 函数。该函数将用户请求的 level 映射到模型支持的最近有效值。


四、Scoped Models 管理

4.1 数据来源

scoped models 来自 AgentSessionConfig.scopedModels,通过 --models CLI 标志传入:

1
2
3
4
5
6
7
8
export interface AgentSessionConfig {
  // ...
  /** Models to cycle through with Ctrl+P (from --models flag) */
  scopedModels?: Array<{
    model: Model<any>;
    thinkingLevel?: ThinkingLevel;  // 可选,指定切换到此模型时使用的 thinking level
  }>;
}

4.2 存取方法

1
2
3
4
5
6
7
8
9
// 读取(只读)
get scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel?: ThinkingLevel }> {
  return this._scopedModels;
}

// 更新(用于插件或动态修改)
setScopedModels(scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>): void {
  this._scopedModels = scopedModels;
}

4.3 在 UI 中的表现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// interactive-mode.ts
this.defaultEditor.onAction("app.model.cycleForward", () => this.cycleModel("forward"));
this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));

private async cycleModel(direction: "forward" | "backward"): Promise<void> {
  const result = await this.session.cycleModel(direction);
  if (result === undefined) {
    const msg = this.session.scopedModels.length > 0
      ? "Only one model in scope"
      : "Only one model available";
    this.showStatus(msg);
  } else {
    const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off"
      ? ` (thinking: ${result.thinkingLevel})` : "";
    this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
  }
}

用户视角的快捷键

快捷键动作
Ctrl+PcycleModel("forward")
Ctrl+Shift+PcycleModel("backward")
Ctrl+TcycleThinkingLevel()

五、Thinking Level 变更事件

5.1 内部事件 — thinking_level_changed

1
2
3
export type AgentSessionEvent =
  // ...
  | { type: "thinking_level_changed"; level: ThinkingLevel }

setThinkingLevel() 发出,用于 UI 更新:

1
2
3
4
case "thinking_level_changed":
  this.footer.invalidate();          // 刷新底部状态栏(显示当前 level)
  this.updateEditorBorderColor();    // 更新编辑器边框颜色(区分 thinking 状态)
  break;

5.2 扩展事件 — thinking_level_select

1
2
3
4
5
export interface ThinkingLevelSelectEvent {
  type: "thinking_level_select";
  level: ThinkingLevel;
  previousLevel: ThinkingLevel;
}

setThinkingLevel() 发出,用于 Extension 监听。只在 level 实际发生变化时发送。


六、外部依赖接口

6.1 ModelRegistry

方法在 Model Management 中的角色
getAvailable()返回所有已配置认证的模型,供 _cycleAvailableModel() 使用
hasConfiguredAuth(model)验证模型是否有可用的认证配置,用于 setModel() 的准入检查
isUsingOAuth(model)判断模型是否使用 OAuth,用于生成友好的错误提示
getApiKeyAndHeaders(model)获取模型的 API key 和请求头,用于认证错误时的提示
find(provider, modelId)在注册表中查找特定模型

6.2 SettingsManager

方法在 Model Management 中的角色
setDefaultModelAndProvider(provider, id)每次模型切换时持久化默认模型
getDefaultThinkingLevel()模型切换时,如果新模型不支持 thinking,从此处恢复默认值
setDefaultThinkingLevel(level)thinking level 变更时持久化

6.3 SessionManager

方法在 Model Management 中的角色
appendModelChange(provider, id)在会话文件中记录模型变更事件,支持分支导航时恢复历史模型
appendThinkingLevelChange(level)在会话文件中记录 thinking level 变更

6.4 AI Compat 层

函数在 Model Management 中的角色
modelsAreEqual(a, b)判断两个模型是否相同,用于 _emitModelSelect() 的去重和 cycleModel() 的索引查找
getSupportedThinkingLevels(model)获取模型支持的 thinking levels,用于 getAvailableThinkingLevels()
clampThinkingLevel(model, level)将 level 钳制到模型支持的范围,用于 _clampThinkingLevel()

七、完整状态映射

模型切换时所有相关状态的变化:

Before:                                After:
─────────────────────────────          ─────────────────────────────
agent.state.model = Cl Haiku           agent.state.model = Cl Sonnet
agent.state.thinkingLevel = "off"      agent.state.thinkingLevel = "high" (scoped 自带)
agent.state.systemPrompt               agent.state.systemPrompt (不变)
         │                                      │
         ▼                                      ▼
sessionManager.appendModelChange()     sessionManager.appendModelChange()
session (当前)                         session (追加一条模型变更 entry)
         │                                      │
         ▼                                      ▼
settingsManager                         settingsManager
  .setDefaultModelAndProvider()           .model  = "claude-sonnet-4-20250514"
                                          .provider = "anthropic"
                                          .defaultThinkingLevel = "high"
         │                                      │
         ▼                                      ▼
_emitModelSelect()                     Extension 收到 model_select 事件
                                         { model: Sonnet, previous: Haiku, source: "cycle" }

八、设计总结

方面设计
双模式切换setModel() 直接设置 vs cycleModel() 循环切换,覆盖不同的使用场景
Scoped 优先--models 标志时限制可切换范围,避免在大模型列表中迷失
Thinking Level 钳制clampThinkingLevel() 确保 level 始终在模型能力范围内,永不抛错
最小化持久化setThinkingLevel() 只在 level 实际变化时写入 session 和 settings
三层存储agent.state(运行时)→ sessionManager(会话持久)→ settingsManager(全局默认)
Extension 可监听model_select / thinking_level_select 双事件支持扩展集成
Scoped 带 Level每个 scoped model 可自带 thinking level,实现一键切换模型+级别
OAuth 降级处理设置 OAuth 模型时返回友好的 /login 提示而非原始错误

一句话总结:Model Management 是 AgentSession 的"方向盘"——通过 setModel/cycleModel 控制使用哪个 LLM,通过 cycleThinkingLevel 控制推理深度,所有变更自动持久化到会话和全局设置,并通过事件通知 UI 和扩展。