Pi Services 层

分析对象是下面这张架构图中的 Services 分组:

text 复制代码
Services
- SettingsManager(设置)
- ResourceLoader(资源加载)
- ModelRuntime(模型运行时)
- 扩展 / 技能 / 模板 / 主题

1. 结论摘要

  1. SettingsManager、ResourceLoader、ModelRuntime 构成 Pi 的"基础设施三件套",由 createAgentSessionServices() 在同一个 cwd 下一起创建。
  2. 三者有清晰的依赖方向:SettingsManager 提供配置,ResourceLoader 依据配置发现资源,ModelRuntime 负责模型目录、凭据和请求发送。扩展再把动态 provider 注册进 ModelRuntime。
  3. 这三项是 cwd-bound 的:会话切换工作目录时,服务会按新目录重新创建,因为项目设置、项目资源和模型配置都可能随目录变化。
  4. "扩展 / 技能 / 模板 / 主题"是资源类别,不是三个新的运行时服务。它们的加载、去重、优先级、诊断和热重载都收敛在 ResourceLoader 中。
  5. 设计上最重要的三条不变量是:项目配置必须经过信任判定;资源冲突要可解释且确定性;加载失败尽量降级为诊断而不是让进程崩溃。

2. 图片条目与代码的对应关系

图中条目 代码实体 主要文件 本质职责
SettingsManager(设置) SettingsManager src/core/settings-manager.ts 全局/项目两层配置的加载、合并、迁移、持久化与访问器
ResourceLoader(资源加载) ResourceLoader 接口 + DefaultResourceLoader src/core/resource-loader.ts 扩展、技能、模板、主题、上下文文件、系统提示的统一发现与装配
ModelRuntime(模型运行时) ModelRuntime src/core/model-runtime.ts 模型目录、provider 组合、凭据解析、可用性快照、请求分发
扩展 / 技能 / 模板 / 主题 Extension / Skill / PromptTemplate / Theme src/core/extensions/、skills.ts、prompt-templates.ts、theme/theme.ts 四类可配置资源,由 ResourceLoader 加载

需要特别澄清"模型运行时"这个译名。ModelRuntime 不是执行模型推理的进程或沙箱,它是 模型访问运行时:管理"有哪些模型、用哪个 provider、凭据从哪来、当前哪些模型可用、请求如何带上正确的认证头和 base URL"。


3. 在整体架构中的位置

3.1 会话启动路径

createAgentSessionServices() 是服务装配入口:

text 复制代码
createAgentSessionServices(cwd, agentDir)
  ├─ ModelRuntime.create({ authPath, modelsPath })
  ├─ SettingsManager.create(cwd, agentDir)
  ├─ new DefaultResourceLoader({ cwd, agentDir, settingsManager })
  ├─ resourceLoader.reload()
  ├─ 把扩展声明的 provider / native provider / virtual model 注册进 ModelRuntime
  └─ 返回 { cwd, agentDir, modelRuntime, settingsManager, resourceLoader, diagnostics }

随后 createAgentSessionFromServices() 把这些服务原样交给 createAgentSession()。这样拆分的目的是:在真正创建会话前,调用方可以先基于目标 cwd 的服务解析模型、思考级别、工具集等会话参数。

对应代码:

  • src/core/agent-session-services.ts:73:AgentSessionServices 接口
  • src/core/agent-session-services.ts:135:createAgentSessionServices()
  • src/core/agent-session-services.ts:214:createAgentSessionFromServices()

3.2 依赖关系

#mermaid-svg-GaWO9aruyTEWX72y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GaWO9aruyTEWX72y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GaWO9aruyTEWX72y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GaWO9aruyTEWX72y .error-icon{fill:#552222;}#mermaid-svg-GaWO9aruyTEWX72y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GaWO9aruyTEWX72y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GaWO9aruyTEWX72y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GaWO9aruyTEWX72y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GaWO9aruyTEWX72y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GaWO9aruyTEWX72y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GaWO9aruyTEWX72y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GaWO9aruyTEWX72y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GaWO9aruyTEWX72y .marker.cross{stroke:#333333;}#mermaid-svg-GaWO9aruyTEWX72y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GaWO9aruyTEWX72y p{margin:0;}#mermaid-svg-GaWO9aruyTEWX72y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GaWO9aruyTEWX72y .cluster-label text{fill:#333;}#mermaid-svg-GaWO9aruyTEWX72y .cluster-label span{color:#333;}#mermaid-svg-GaWO9aruyTEWX72y .cluster-label span p{background-color:transparent;}#mermaid-svg-GaWO9aruyTEWX72y .label text,#mermaid-svg-GaWO9aruyTEWX72y span{fill:#333;color:#333;}#mermaid-svg-GaWO9aruyTEWX72y .node rect,#mermaid-svg-GaWO9aruyTEWX72y .node circle,#mermaid-svg-GaWO9aruyTEWX72y .node ellipse,#mermaid-svg-GaWO9aruyTEWX72y .node polygon,#mermaid-svg-GaWO9aruyTEWX72y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GaWO9aruyTEWX72y .rough-node .label text,#mermaid-svg-GaWO9aruyTEWX72y .node .label text,#mermaid-svg-GaWO9aruyTEWX72y .image-shape .label,#mermaid-svg-GaWO9aruyTEWX72y .icon-shape .label{text-anchor:middle;}#mermaid-svg-GaWO9aruyTEWX72y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GaWO9aruyTEWX72y .rough-node .label,#mermaid-svg-GaWO9aruyTEWX72y .node .label,#mermaid-svg-GaWO9aruyTEWX72y .image-shape .label,#mermaid-svg-GaWO9aruyTEWX72y .icon-shape .label{text-align:center;}#mermaid-svg-GaWO9aruyTEWX72y .node.clickable{cursor:pointer;}#mermaid-svg-GaWO9aruyTEWX72y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GaWO9aruyTEWX72y .arrowheadPath{fill:#333333;}#mermaid-svg-GaWO9aruyTEWX72y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GaWO9aruyTEWX72y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GaWO9aruyTEWX72y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GaWO9aruyTEWX72y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GaWO9aruyTEWX72y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GaWO9aruyTEWX72y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GaWO9aruyTEWX72y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GaWO9aruyTEWX72y .cluster text{fill:#333;}#mermaid-svg-GaWO9aruyTEWX72y .cluster span{color:#333;}#mermaid-svg-GaWO9aruyTEWX72y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GaWO9aruyTEWX72y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GaWO9aruyTEWX72y rect.text{fill:none;stroke-width:0;}#mermaid-svg-GaWO9aruyTEWX72y .icon-shape,#mermaid-svg-GaWO9aruyTEWX72y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GaWO9aruyTEWX72y .icon-shape p,#mermaid-svg-GaWO9aruyTEWX72y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GaWO9aruyTEWX72y .icon-shape .label rect,#mermaid-svg-GaWO9aruyTEWX72y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GaWO9aruyTEWX72y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GaWO9aruyTEWX72y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GaWO9aruyTEWX72y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 设置/资源路径/信任状态
扩展注册 provider
默认模型/思考级别/重试等
模型与凭据
工具/命令/技能/提示/主题
CLI / SDK / RPC
createAgentSessionServices
SettingsManager
ResourceLoader
ModelRuntime
AgentSession

SettingsManager 不依赖另外两者;ResourceLoader 依赖 SettingsManager 和 PackageManager;ModelRuntime 不依赖 ResourceLoader,只通过扩展注册接口被反向注入。这种方向避免了"模型层反向依赖资源层"的循环。

3.3 为什么是 cwd-bound

项目设置位于 <cwd>/.pi/settings.json(目录名由 CONFIG_DIR_NAME 决定,默认 .pi),项目资源位于 <cwd>/.pi/{extensions,skills,prompts,themes},模型配置虽然默认在 agent 目录,但项目信任状态会影响项目设置是否生效。因此切换 cwd 后继续复用旧服务会产生"旧目录配置作用在新目录"的错误。AgentSessionRuntime 的 newSession()、switchSession()、fork() 等操作会重建服务。


4. SettingsManager(设置)

4.1 职责边界

SettingsManager 只做配置,不做资源加载,也不碰模型请求。它负责:

  • 读取全局设置 <agentDir>/settings.json
  • 读取项目设置 <cwd>/.pi/settings.json
  • 深合并两层设置,项目优先
  • 迁移旧格式
  • 提供类型化访问器与默认值
  • 串行、加锁地写回文件
  • 记录加载/写入错误
  • 维护项目信任状态

类定义见 src/core/settings-manager.ts:272。

4.2 两层配置与合并规则

构造时先分别加载 global 和 project,再执行 deepMergeSettings(global, project):

  • 嵌套对象递归合并。
  • 数组整体替换,不做元素级合并。
  • defaultTools 是例外:纯名称列表替换继承值,只有 +name / -name 的列表则修改继承值。
  • undefined 的覆盖值被忽略,不会抹掉基础值。

合并逻辑在 src/core/settings-manager.ts:61(deepMergeObjects)和 :143(deepMergeSettings)。

这意味着"项目设置覆盖用户设置"不是整文件替换,而是字段级覆盖;但数组字段(如 skills、extensions)在设置合并阶段是替换的。资源"数组会合并"的效果来自后面的 PackageManager:它分别读取 global 和 project 的数组再一起解析,而不是靠 SettingsManager 合并。

4.3 项目信任

项目设置只有在项目被信任时才加载:

  • loadFromStorage() 在 scope === "project" && !projectTrusted 时直接返回 {}。
  • setProjectTrusted(false) 会清空项目设置并重新计算有效设置。
  • 所有项目设置写入都会经过 assertProjectTrustedForWrite(),未信任时直接抛错。

对应代码:src/core/settings-manager.ts:366、:475、:555。

这个边界很重要:项目目录里的 .pi/settings.json 可以指定扩展、技能、shell 前缀等,属于可执行配置,不能在用户确认信任前生效。

4.4 持久化机制

写入路径有三个关键设计:

  1. 文件锁 :FileSettingsStorage 用 proper-lockfile 的 lockSync,遇到 ELOCKED 最多重试 10 次、每次约 20ms。
  2. 写队列 :SettingsManager 内部维护 writeQueue,同一实例的写入串行执行,避免并发覆盖。
  3. 只写修改字段 :modifiedFields / modifiedNestedFields 记录本会话改过哪些字段;写盘时先读当前文件,只把这些字段覆盖回去。这样外部进程同时修改其他字段时不会被整文件覆盖。

对应代码:src/core/settings-manager.ts:197、:576、:598、:629。

另外,目录只在真正需要写入时创建;只读启动不会在项目里创建 .pi。

4.5 迁移与校验

migrateSettings() 处理历史格式:

  • queueMode → steeringMode
  • websockets: boolean → transport: "websocket" | "sse"
  • 旧的 skills 对象格式 → 新的数组格式,并迁移 enableSkillCommands
  • retry.maxDelayMs → retry.provider.maxRetryDelayMs

对应代码:src/core/settings-manager.ts:397。

设置文件本身不做全量 schema 校验,而是在具体访问器里做范围/类型校验,例如:

  • compaction.reserveTokens 必须是非负安全整数,否则抛错。
  • httpIdleTimeoutMs 必须是非负有限数。
  • imageWidthCells、editorPaddingX 等会被夹到合法区间。

这种策略让旧文件或手写文件仍能启动,错误延迟到实际使用该字段时暴露。

4.6 访问器面

SettingsManager 暴露了大量窄访问器,而不是让调用方直接读一个大对象。例如:

  • 模型:getDefaultProvider()、getDefaultModel()、getDefaultThinkingLevel()、getModelThinkingLevel()
  • 压缩:getCompactionSettings(model?),支持按 provider/modelId 覆盖
  • 重试:getRetrySettings()、getProviderRetrySettings()
  • 终端:getThemeSetting()、getTerminalCapabilityOverrides()、getImageWidthCells()
  • 资源:getPackages()、getExtensionPaths()、getSkillPaths()、getPromptTemplatePaths()、getThemePaths()

好处是默认值和类型收敛在一处,调用方不需要重复写 ?? DEFAULT。getSettings() 返回深拷贝,避免外部修改内部状态。

4.7 失败语义

  • 加载失败:记录到 errors,对应 scope 的有效设置为 {};drainErrors() 交给上层决定是否展示或中止。
  • 全局设置加载失败后,save() 直接返回,避免用不完整状态覆盖坏文件。
  • 写入失败:enqueueWrite() 捕获并记录,不向调用方抛异步异常。
  • 项目未信任写入:同步抛错,因为这是调用方的逻辑错误,不是 I/O 故障。

这套语义把"配置损坏"与"程序 bug"区分开了。

4.8 观察

  • 优点:两层合并、字段级写回、锁 + 队列、信任边界都很实用。
  • 代价:读文件是同步 I/O;设置很多时访问器数量会持续膨胀;defaultTools 的特殊合并规则增加了理解成本。
  • 风险点:设置文件没有启动时全量校验,坏值可能延迟到运行中才报错;但这也换来了对旧格式的容忍。

5. ResourceLoader(资源加载)

5.1 职责边界

ResourceLoader 是资源发现与装配层。它不执行扩展逻辑,也不发模型请求;它负责把磁盘、包、CLI 参数、内置资源统一成会话可用的对象集合。

接口定义在 src/core/resource-loader.ts:153,默认实现是 DefaultResourceLoader(:315)。接口包含:

ts 复制代码
getExtensions()
getSkills()
getPrompts()
getThemes()
getAgentsFiles()
getSystemPrompt()
getSystemPromptSource()
getAppendSystemPrompt()
getAppendSystemPromptSources()
extendResources(paths)
reload(options?)

5.2 reload 的完整流程

reload() 是核心入口(src/core/resource-loader.ts:513),顺序如下:

  1. 重置扩展加载计时;如果之前加载过,清空扩展模块缓存。
  2. 如果调用方提供 resolveProjectTrust,先用"项目未信任"状态加载一次扩展,询问用户是否信任项目,再写回信任状态。这一步让个人扩展和 CLI 显式扩展可以参与 project_trust 事件,而项目扩展尚不加载。
  3. 调用 settingsManager.reload(),按最终信任状态重新读取设置。
  4. packageManager.resolve() 解析四类资源:extensions、skills、prompts、themes。
  5. packageManager.resolveExtensionSources(additionalExtensionPaths, { temporary: true }) 解析 CLI -e 等临时扩展源。
  6. 合并路径并按规范路径去重。
  7. 加载扩展:文件扩展、builtin:<name> 内置扩展、内联工厂扩展;处理可替换扩展和冲突诊断。
  8. 加载技能、提示模板、主题。
  9. 加载上下文文件(AGENTS.md 等)。
  10. 解析系统提示和追加系统提示。
  11. 标记 loaded = true。

5.3 扩展加载

扩展是 TypeScript/JavaScript 模块,默认导出工厂函数,接收 ExtensionAPI,可注册工具、命令、快捷键、CLI flag、provider、事件处理器、渲染器和终端 UI。

加载细节:

  • 文件扩展通过 jiti 直接执行,本地 TS 不需要预编译。
  • 目录扩展要求 index.ts 或 index.js 入口。
  • builtin:<name> 不读磁盘,而是从 DefaultResourceLoader 构造时传入的 builtinExtensions 映射中取工厂。
  • 内联扩展(SDK 传入的 extensionFactories)在最后追加。
  • replaceable 扩展如果与不可替换扩展注册了同名 tool/command/flag,会被省略并产生 warning。例如第三方 MCP 扩展注册 /mcp 时,内置 MCP 扩展不会同时加载。
  • 普通扩展之间的同名冲突不会被静默解决:所有扩展保留,冲突写入 errors,实际优先级由加载顺序决定。
  • 扩展包如果把 @earendil-works/pi-* 写进 dependencies 而不是 peerDependencies,会产生 warning,因为可能装入第二份运行时模块。

对应代码:src/core/resource-loader.ts:577(路径合并)、:680(当前扩展集)、:751(最终扩展集)、:1250(冲突检测)。

5.4 技能加载

技能是带 SKILL.md 的目录,遵循 Agent Skills 规范。启动时只把 name、description、location 放进系统提示,完整指令由模型在需要时读取。

发现规则(src/core/skills.ts:168):

  • 目录里有 SKILL.md 就视为技能根,不再向下递归。
  • 否则加载根目录下的直接 .md 子文件(仅当 includeRootFiles 为真)。
  • 继续递归子目录寻找 SKILL.md。
  • 跳过隐藏目录和 node_modules。
  • 支持 .gitignore、.ignore、.fdignore。

校验规则(src/core/skills.ts:277):

  • description 必填;缺失则不加载。
  • name 必须是小写字母、数字、连字符,长度不超过 64,不能以连字符开头/结尾,不能有连续连字符。
  • description 不超过 1024 字符。
  • 名称冲突时保留第一个发现的技能,并产生 collision 诊断。
  • disable-model-invocation: true 的技能不进入系统提示,只能通过 /skill:name 显式调用。

5.5 提示模板加载

提示模板是 Markdown 文件,文件名即命令名(去掉 .md),例如 review.md → /review。

  • 只加载目录下的直接 .md 子文件,不递归。
  • frontmatter 可写 description 和 argument-hint。
  • 没有 description 时取正文第一条非空行,截断到 60 字符。
  • 同名模板保留第一个,产生 collision 诊断。
  • 支持 $1、$@、$ARGUMENTS、${1:-default}、${@:N}、${@:N:L} 等替换,参数按 shell 风格引号解析。
  • 扩展的 input 事件先于模板展开看到原始输入。

对应代码:src/core/prompt-templates.ts:110、:227、:309。

5.6 主题加载

主题是 JSON 文件,定义终端 UI 颜色和 HTML 导出颜色。

  • 目录中的 .json 文件会被加载。
  • name 必填,不能包含 /,不能是保留名 system。
  • 支持 vars 变量引用、colors 角色颜色、export 导出背景覆盖。
  • 同名主题保留第一个,产生 collision 诊断。
  • 加载时使用终端能力探测结果和设置中的 terminal.* 覆盖来决定色彩模式。
  • 只有 <agentDir>/themes/<name>.json 的当前主题会被热重载;其他来源需要 /reload。

对应代码:src/core/resource-loader.ts:891、:1043。

5.7 上下文文件与系统提示

上下文文件独立于 .pi 配置,不需要项目信任:

  • 每个目录的候选顺序:AGENTS.override.md、AGENTS.md、AGENTS.MD、CLAUDE.md、CLAUDE.MD。
  • 加载 agent 目录、当前目录及所有祖先目录。
  • 同一文件只加载一次。
  • 对嵌套 linked worktree 做了"主仓库同名文件被 worktree 文件遮蔽"的处理,避免同一逻辑仓库的上下文重复应用。

系统提示发现顺序(src/core/resource-loader.ts:1213):

  1. 显式传入的 systemPrompt / appendSystemPrompt
  2. 项目 .pi/SYSTEM.md 或 .pi/APPEND_SYSTEM.md(仅项目已信任)
  3. 全局 <agentDir>/SYSTEM.md 或 <agentDir>/APPEND_SYSTEM.md

项目文件优先于全局文件,且同名文件不合并。

5.8 资源优先级与冲突

PackageManager 给资源元数据计算优先级(src/core/package-manager.ts:198),数字越小优先级越高:

优先级 来源
0 项目设置显式条目(source: "local", scope: "project")
1 项目自动发现(source: "auto", scope: "project")
2 用户设置显式条目
3 用户自动发现
4 包资源(origin: "package")
5 内置扩展

解析结果按该优先级排序,因此"同名第一个获胜"得到的是项目优先、显式优先、包最后。CLI 临时路径在 ResourceLoader.mergePaths() 中作为 primary 放在前面,所以 CLI 指定项优先于设置和包。

扩展不按"第一个获胜"丢弃,而是全部加载;冲突通过诊断报告,replaceable 扩展例外。

5.9 诊断与来源信息

每个资源都带 SourceInfo,描述:

  • source:local、auto、package、builtin、cli 等
  • scope:user、project、temporary
  • origin:top-level、package
  • baseDir:相对路径解析基准

诊断类型包括 warning、error、collision。大部分资源问题不会中止启动,而是由上层决定如何展示。这符合"配置错误不应让整个 Agent 不可用"的原则。

5.10 运行时扩展资源

extendResources() 允许扩展在加载后追加技能、模板、主题路径:

  • 路径会合并进 lastSkillPaths / lastPromptPaths / lastThemePaths
  • 立即重新加载对应资源
  • 记录来源信息,保证诊断能指回扩展

它不允许运行时追加扩展,因为扩展加载涉及模块缓存和事件总线,重载需要走 reload()。

5.11 观察

  • 优点:四类资源共用一套发现、优先级、去重和诊断机制;信任边界清晰;来源可追溯。
  • 代价:ResourceLoader 职责很重,reload() 是一条较长的流水线;扩展、技能、模板、主题的加载规则各不相同,理解成本集中在单文件里。
  • 风险点:扩展在 Pi 进程内以完整权限执行;项目扩展必须在信任后加载;名称冲突虽然可诊断,但用户需要理解"先发现者获胜"。

6. ModelRuntime(模型运行时)

6.1 职责边界

ModelRuntime 实现 @earendil-works/pi-ai 的 Models 接口(src/core/model-runtime.ts:173),是 coding-agent 和 SDK 共用的模型访问层。它负责:

  • 组合 provider 目录
  • 解析凭据(存储的 API key、OAuth、运行时 API key、环境变量、models.json 配置)
  • 维护"全部模型"和"当前可用模型"快照
  • 在请求前解析认证头、环境变量、base URL
  • 分发 chat / simple / deferred / image / classifier 请求
  • 处理虚拟模型的请求路由
  • 支持扩展动态注册 provider 和模型

6.2 Provider 组合

provider 来源有四类:

  1. 内置目录 :@earendil-works/pi-ai/providers/all 的 builtinProviders()
  2. models.json 配置 :ModelConfig,可覆盖或新增 provider
  3. 扩展注册 :registerProvider() 注册声明式配置,registerNativeProvider() 注册完整 Provider 实现
  4. 虚拟模型 :registerVirtualModel() 在某个 provider 下注册可路由到其他模型的虚拟条目

composeProvider()(src/core/model-runtime.ts:308)以 native 扩展 provider 或内置 provider 为 base,再叠加 models.json 配置和扩展配置。没有 overlay 时直接返回内置 provider,保证其认证和流式行为不被包装改变。

组合失败不会丢弃 provider:错误记录到 compositionErrors,getError() 汇总展示,base provider 继续可用。

6.3 凭据与认证

ModelRuntime.create() 默认使用 AuthStorage 读取 <agentDir>/auth.json,并包装为 RuntimeCredentials。凭据来源包括:

  • 存储的 API key
  • OAuth 凭据
  • 运行时注入的 API key(setRuntimeApiKey)
  • 环境变量
  • models.json 中的 provider 配置

认证相关操作按 provider 串行化(enqueueCredentialOperation,:579),避免同一 provider 的 login/logout/setKey 并发交错。凭据提交成功后,会重新组合 provider、刷新模型、刷新可用性;如果本地同步失败,抛出 CredentialSynchronizationError,明确区分"凭据已提交"和"本地状态未同步"。

6.4 可用性快照

ModelRuntimeSnapshot 保存:

  • all:目录中的全部模型
  • available:当前有凭据可用的模型
  • configuredProviders:有认证配置的 provider
  • storedProviders:有存储凭据的 provider
  • auth:每个 provider 的认证检查结果

刷新使用序号(availabilityRefreshSeq、providerAvailabilitySeq)防止旧请求覆盖新结果。getAvailableSnapshot() 返回同步快照,适合启动时立即选模型;getAvailable() 会触发一次完整刷新。

这套设计解决了一个实际问题:模型选择器需要在异步认证检查完成前就显示候选模型,因此先给"暂定可用"状态,再由后续刷新校正。

6.5 请求路径

prepareRequest()(src/core/model-runtime.ts:658)统一处理:

  • 查找 provider,未知则抛 ModelsError("provider", ...)
  • 解析认证,未配置则抛 ModelsError("auth", ...)
  • 合并认证头、调用方头、transformHeaders
  • 合并环境变量
  • 如果认证结果带 baseUrl,替换模型上的 base URL

之后:

  • stream() / complete():标准 chat
  • streamSimple() / completeSimple():简化 chat;虚拟模型会先 resolveModel() 路由到物理模型,并把输出预算限制到目标模型
  • streamDeferred() / fetchDeferred() / cancelDeferred():延迟响应
  • generateImages():图像生成
  • classify():分类模型

所有流式入口都用 lazyStream() 包装,认证和 provider 解析发生在真正消费流时,而不是调用时。

6.6 刷新与动态注册

  • refresh() 重新加载 models.json,重建 provider,按需联网刷新目录,然后刷新可用性。
  • registerProvider() / registerNativeProvider() 立即重组 provider 并异步刷新。
  • unregisterProvider() 删除注册并刷新。
  • registerVirtualModel() / unregisterVirtualModel() 管理虚拟模型。
  • registerProvider() 会先独立校验新配置,校验失败不会污染已有注册。
  • 重复注册会合并已定义字段,保留未定义字段,兼容旧 ModelRegistry 行为。

6.7 虚拟模型

虚拟模型是一个"入口模型",其 route() 根据消息历史、思考级别、失败信息等选择物理模型。resolveModel()(:1003)会:

  1. 确认虚拟模型存在
  2. 调用路由函数
  3. 确认目标是物理模型
  4. 确认目标 provider 有凭据
  5. 把思考级别夹到目标模型支持的范围

这让"按任务自动选模型"可以复用现有的认证、流式和重试基础设施,而不需要另建一套请求路径。

6.8 失败语义

  • 未知 provider / 未配置认证:请求时抛 ModelsError。
  • provider 组合失败:记录错误,保留 base provider。
  • 可用性刷新失败:记录 availabilityError,已刷新模型仍可用。
  • 图像/分类失败:返回结构化错误结果,而不是抛异常。
  • 联网刷新默认关闭;PI_OFFLINE 环境变量也会禁用联网。

6.9 观察

  • 优点:把模型目录、认证、可用性、请求准备集中在一层,扩展和 SDK 都能复用;并发和过期结果处理比较严谨。
  • 代价:ModelRuntime 同时承担目录管理、凭据管理、请求编排和虚拟路由,职责偏多;provider 组合有"内置 + 配置 + 扩展 + 虚拟"四种叠加,理解成本高。
  • 风险点:凭据操作失败后的状态同步错误需要调用方正确处理;动态注册与刷新是异步的,快照可能短暂滞后。

7. 扩展 / 技能 / 模板 / 主题

这四类资源经常被并列,但它们的"可执行程度"和"进入模型上下文的方式"不同。

维度 扩展 Extension 技能 Skill 提示模板 Prompt Template 主题 Theme
本质 可执行 TypeScript 模块 按需加载的指令包 可复用的消息文本 终端颜色配置
入口 默认导出工厂 目录中的 SKILL.md .md 文件 .json 文件
是否执行代码 是,进程内 否,模型读取文本 否 否
启动时进入上下文 注册工具/命令/事件 只放 name/description/location 不放,调用时展开 不放
调用方式 工具、/command、事件 模型自动读取或 /skill:name /name /settings 选择
项目信任 项目扩展需信任 项目技能需信任 项目模板需信任 项目主题需信任
冲突策略 全部加载,报告冲突;replaceable 可被替换 同名先到先得 同名先到先得 同名先到先得
分发方式 文件、目录、Pi package 目录、Pi package 文件、目录、Pi package 文件、目录、Pi package

7.1 扩展

扩展是唯一能新增可执行集成点的资源:注册工具、命令、快捷键、CLI flag、provider、事件处理器、渲染器。它运行在 Pi 进程内,拥有与 Pi 相同的操作系统权限,因此信任模型最严格。

生命周期上有一个重要约束:不要在工厂函数里启动进程、socket、watcher 或 timer,因为有些调用只加载扩展而不启动会话。长生命周期资源应放在 session_start,并在 session_shutdown 中幂等关闭。

7.2 技能

技能解决"指令太多,不能全塞进上下文"的问题。启动时只暴露路由信息,模型判断相关后再读取完整 SKILL.md。这既节省 token,也让技能可以携带脚本、参考文档和资产。

技能的关键设计是描述质量:描述要同时说明"做什么"和"什么时候用",否则模型无法可靠路由。

7.3 提示模板

提示模板是轻量级复用机制,不引入可执行行为。它适合"同一段提示反复使用"的场景,例如代码审查、提交信息生成。参数替换让它比纯文本片段更灵活,但仍保持在消息层面。

7.4 主题

主题只影响呈现,不影响 Agent 行为。system 主题从终端读取前景色、背景色和 ANSI 调色板,并按 WCAG 对比度约束生成颜色;自定义主题是严格 JSON,未知字段会被拒绝。主题冲突按名称去重,先发现者获胜。

7.5 Pi Packages

四类资源都可以通过 Pi package 分发。SettingsManager 的 packages 字段声明 npm、git 或本地包;PackageManager 解析包内的资源并赋予 origin: "package",其优先级低于项目/用户显式配置和自动发现。包是"分发单元",不是新的资源类型。


8. 服务协作与启动顺序

Extensions ModelRuntime PackageManager DefaultResourceLoader SettingsManager createAgentSessionServices CLI/SDK Extensions ModelRuntime PackageManager DefaultResourceLoader SettingsManager createAgentSessionServices CLI/SDK #mermaid-svg-sokczoRguqgdFtik{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sokczoRguqgdFtik .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sokczoRguqgdFtik .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sokczoRguqgdFtik .error-icon{fill:#552222;}#mermaid-svg-sokczoRguqgdFtik .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sokczoRguqgdFtik .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sokczoRguqgdFtik .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sokczoRguqgdFtik .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sokczoRguqgdFtik .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sokczoRguqgdFtik .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sokczoRguqgdFtik .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sokczoRguqgdFtik .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sokczoRguqgdFtik .marker.cross{stroke:#333333;}#mermaid-svg-sokczoRguqgdFtik svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sokczoRguqgdFtik p{margin:0;}#mermaid-svg-sokczoRguqgdFtik .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sokczoRguqgdFtik text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-sokczoRguqgdFtik .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sokczoRguqgdFtik .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-sokczoRguqgdFtik .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-sokczoRguqgdFtik .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-sokczoRguqgdFtik #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-sokczoRguqgdFtik .sequenceNumber{fill:white;}#mermaid-svg-sokczoRguqgdFtik #sequencenumber{fill:#333;}#mermaid-svg-sokczoRguqgdFtik #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-sokczoRguqgdFtik .messageText{fill:#333;stroke:none;}#mermaid-svg-sokczoRguqgdFtik .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sokczoRguqgdFtik .labelText,#mermaid-svg-sokczoRguqgdFtik .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-sokczoRguqgdFtik .loopText,#mermaid-svg-sokczoRguqgdFtik .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-sokczoRguqgdFtik .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sokczoRguqgdFtik .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-sokczoRguqgdFtik .noteText,#mermaid-svg-sokczoRguqgdFtik .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-sokczoRguqgdFtik .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sokczoRguqgdFtik .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sokczoRguqgdFtik .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sokczoRguqgdFtik .actorPopupMenu{position:absolute;}#mermaid-svg-sokczoRguqgdFtik .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-sokczoRguqgdFtik .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sokczoRguqgdFtik .actor-man circle,#mermaid-svg-sokczoRguqgdFtik line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-sokczoRguqgdFtik :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} cwd, agentDircreate(authPath, modelsPath)create(cwd, agentDir)new DefaultResourceLoader(...)reload()reload()resolve()extensions/skills/prompts/themesload extensionstools/commands/providers/eventsload skills/prompts/themes/context/system promptresources + diagnosticsregister pending providers/virtual modelsrefresh(allowNetwork=false)services + diagnostics

几个值得注意的协作点:

  1. 扩展可以影响模型层 :扩展在加载时把 provider 注册请求放进 extensionsResult.runtime.pendingProviderRegistrations,由 createAgentSessionServices() 统一转交给 ModelRuntime。这样 ResourceLoader 不需要直接依赖 ModelRuntime。
  2. 设置影响资源发现 :PackageManager.resolve() 读取 SettingsManager 的 global/project 设置,解析 packages、extensions、skills、prompts、themes 和内置扩展开关。
  3. 信任先于项目资源 :如果调用方提供 resolveProjectTrust,先以未信任状态加载一次扩展,询问后再加载项目资源。
  4. 诊断集中返回 :服务创建不打印、不退出,而是返回 AgentSessionRuntimeDiagnostic[],由应用层决定展示方式。
  5. 失败尽量局部化:单个扩展加载失败、单个技能格式错误、单个 provider 组合失败,都不会阻止其他资源或服务可用。

9. 设计评价

9.1 做得好的地方

  • 边界清楚:配置、资源发现、模型访问三层分开,测试和替换都容易。
  • 信任模型完整:项目设置、项目扩展、项目技能、项目模板、项目主题都在信任后加载;未信任时项目设置写入直接拒绝。
  • 确定性优先级:资源优先级有明确排序,冲突有诊断,不依赖文件系统遍历顺序。
  • 降级友好:大部分配置/资源错误变成诊断,而不是启动失败。
  • 可观测性 :SourceInfo、ResourceDiagnostic、getError()、计时器让问题可定位。
  • 扩展能力强 :扩展能注册工具、命令、provider、虚拟模型和 UI,同时通过 replaceable 和冲突诊断避免"静默双加载"。

9.2 需要付出的成本

  • 概念数量多 :source / scope / origin / baseDir、四类资源、五种 provider 来源,新贡献者需要时间建立心智模型。
  • 单文件职责偏重 :DefaultResourceLoader.reload() 是一条长流水线,ModelRuntime 同时管目录、凭据和请求。
  • 同步 I/O:设置加载和资源扫描大量使用同步文件 API,启动时可能成为瓶颈;项目很大时技能递归扫描尤其明显。
  • 配置校验延迟:设置文件不做全量 schema 校验,坏值可能到运行中才暴露。
  • 扩展安全边界依赖用户判断:扩展在进程内拥有完整权限,沙箱不是默认能力。

9.3 值得保持的不变量

  1. 项目资源必须在信任后加载。
  2. 资源冲突必须确定性解决并可诊断。
  3. 服务按 cwd 重建,不跨目录复用。
  4. 单个资源/扩展/provider 失败不应拖垮整个会话。
  5. 模型请求的认证头和 base URL 只能由 ModelRuntime 统一解析,避免各调用方重复实现。

10. 关键代码索引

主题 位置
服务装配入口 packages/coding-agent/src/core/agent-session-services.ts:135
AgentSessionServices 定义 packages/coding-agent/src/core/agent-session-services.ts:73
SettingsManager 类 packages/coding-agent/src/core/settings-manager.ts:272
设置深合并 packages/coding-agent/src/core/settings-manager.ts:143
设置写队列 packages/coding-agent/src/core/settings-manager.ts:576
只写修改字段 packages/coding-agent/src/core/settings-manager.ts:598
设置迁移 packages/coding-agent/src/core/settings-manager.ts:397
ResourceLoader 接口 packages/coding-agent/src/core/resource-loader.ts:153
DefaultResourceLoader packages/coding-agent/src/core/resource-loader.ts:315
资源重载流水线 packages/coding-agent/src/core/resource-loader.ts:513
扩展冲突检测 packages/coding-agent/src/core/resource-loader.ts:1250
资源优先级 packages/coding-agent/src/core/package-manager.ts:198
包与设置资源解析 packages/coding-agent/src/core/package-manager.ts:926
技能发现 packages/coding-agent/src/core/skills.ts:168
技能系统提示格式 packages/coding-agent/src/core/skills.ts:358
提示模板加载 packages/coding-agent/src/core/prompt-templates.ts:227
提示模板参数替换 packages/coding-agent/src/core/prompt-templates.ts:76
ModelRuntime 类 packages/coding-agent/src/core/model-runtime.ts:173
provider 组合 packages/coding-agent/src/core/model-runtime.ts:308
可用性刷新 packages/coding-agent/src/core/model-runtime.ts:342
凭据操作串行化 packages/coding-agent/src/core/model-runtime.ts:579
请求准备 packages/coding-agent/src/core/model-runtime.ts:658
虚拟模型路由 packages/coding-agent/src/core/model-runtime.ts:1003
配置文档 packages/coding-agent/docs/configuration.md
设置参考 packages/coding-agent/docs/settings.md
扩展文档 packages/coding-agent/docs/extensions.md
技能文档 packages/coding-agent/docs/skills.md
模板文档 packages/coding-agent/docs/prompt-templates.md
主题文档 packages/coding-agent/docs/themes.md

11. 一句话总结

这张图描述的是 Pi 的"会话基础设施层":SettingsManager 决定"用什么配置",ResourceLoader 决定"有哪些能力与指令可用",ModelRuntime 决定"用哪个模型、拿什么凭据、把请求发到哪里";而扩展、技能、模板、主题是被 ResourceLoader 统一装配的四类资源,其中只有扩展是可执行代码。

相关推荐
hpoenixf13 小时前
从 Trace 到可信观测:我们的 Agent 可观测体系怎样一步步建立起来
人工智能·agent
hpoenixf13 小时前
从能读取到能研究:我们如何把 agent的 PDF tool升级为可追溯的多模态能力
agent
goehou13 小时前
Agent 接飞书实战:回调验签、事件去重与异步回复(附踩坑)
实战·飞书·agent·mcp·im集成
Rosanci14 小时前
多个 AI 会话同时改一个项目,怎样避免任务互相踩?
人工智能·agent·ai 编程
七夜zippoe16 小时前
Agent 接入企业微信/钉钉:渠道对接实
ai·钉钉·企业微信·agent·渠道对接
aichitang202417 小时前
QCU 2.0.0:补齐 Windows、Linux 与 GNOME Wayland
linux·运维·windows·ai·agent·ai编程
Autumn_ing18 小时前
AI Agent工作台能做什么?一个产研任务从资料到交付怎么走
ai·agent·产品经理·墨刀·ai agent·prd·墨刀ai客户端
拍手笑沙鸥19 小时前
系列8之「超体」闭环迭代机制:让智能体"越用越聪明"的自我进化
agent