
分析对象是下面这张架构图中的 Services 分组:
text
Services
- SettingsManager(设置)
- ResourceLoader(资源加载)
- ModelRuntime(模型运行时)
- 扩展 / 技能 / 模板 / 主题
1. 结论摘要
SettingsManager、ResourceLoader、ModelRuntime构成 Pi 的"基础设施三件套",由createAgentSessionServices()在同一个cwd下一起创建。- 三者有清晰的依赖方向:
SettingsManager提供配置,ResourceLoader依据配置发现资源,ModelRuntime负责模型目录、凭据和请求发送。扩展再把动态 provider 注册进ModelRuntime。 - 这三项是 cwd-bound 的:会话切换工作目录时,服务会按新目录重新创建,因为项目设置、项目资源和模型配置都可能随目录变化。
- "扩展 / 技能 / 模板 / 主题"是资源类别,不是三个新的运行时服务。它们的加载、去重、优先级、诊断和热重载都收敛在
ResourceLoader中。 - 设计上最重要的三条不变量是:项目配置必须经过信任判定;资源冲突要可解释且确定性;加载失败尽量降级为诊断而不是让进程崩溃。
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 持久化机制
写入路径有三个关键设计:
- 文件锁 :
FileSettingsStorage用proper-lockfile的lockSync,遇到ELOCKED最多重试 10 次、每次约 20ms。 - 写队列 :
SettingsManager内部维护writeQueue,同一实例的写入串行执行,避免并发覆盖。 - 只写修改字段 :
modifiedFields/modifiedNestedFields记录本会话改过哪些字段;写盘时先读当前文件,只把这些字段覆盖回去。这样外部进程同时修改其他字段时不会被整文件覆盖。
对应代码:src/core/settings-manager.ts:197、:576、:598、:629。
另外,目录只在真正需要写入时创建;只读启动不会在项目里创建 .pi。
4.5 迁移与校验
migrateSettings() 处理历史格式:
queueMode→steeringModewebsockets: 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),顺序如下:
- 重置扩展加载计时;如果之前加载过,清空扩展模块缓存。
- 如果调用方提供
resolveProjectTrust,先用"项目未信任"状态加载一次扩展,询问用户是否信任项目,再写回信任状态。这一步让个人扩展和 CLI 显式扩展可以参与project_trust事件,而项目扩展尚不加载。 - 调用
settingsManager.reload(),按最终信任状态重新读取设置。 packageManager.resolve()解析四类资源:extensions、skills、prompts、themes。packageManager.resolveExtensionSources(additionalExtensionPaths, { temporary: true })解析 CLI-e等临时扩展源。- 合并路径并按规范路径去重。
- 加载扩展:文件扩展、
builtin:<name>内置扩展、内联工厂扩展;处理可替换扩展和冲突诊断。 - 加载技能、提示模板、主题。
- 加载上下文文件(
AGENTS.md等)。 - 解析系统提示和追加系统提示。
- 标记
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):
- 显式传入的
systemPrompt/appendSystemPrompt - 项目
.pi/SYSTEM.md或.pi/APPEND_SYSTEM.md(仅项目已信任) - 全局
<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、temporaryorigin:top-level、packagebaseDir:相对路径解析基准
诊断类型包括 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 来源有四类:
- 内置目录 :
@earendil-works/pi-ai/providers/all的builtinProviders() models.json配置 :ModelConfig,可覆盖或新增 provider- 扩展注册 :
registerProvider()注册声明式配置,registerNativeProvider()注册完整Provider实现 - 虚拟模型 :
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:有认证配置的 providerstoredProviders:有存储凭据的 providerauth:每个 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():标准 chatstreamSimple()/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)会:
- 确认虚拟模型存在
- 调用路由函数
- 确认目标是物理模型
- 确认目标 provider 有凭据
- 把思考级别夹到目标模型支持的范围
这让"按任务自动选模型"可以复用现有的认证、流式和重试基础设施,而不需要另建一套请求路径。
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
几个值得注意的协作点:
- 扩展可以影响模型层 :扩展在加载时把 provider 注册请求放进
extensionsResult.runtime.pendingProviderRegistrations,由createAgentSessionServices()统一转交给ModelRuntime。这样ResourceLoader不需要直接依赖ModelRuntime。 - 设置影响资源发现 :
PackageManager.resolve()读取SettingsManager的 global/project 设置,解析packages、extensions、skills、prompts、themes和内置扩展开关。 - 信任先于项目资源 :如果调用方提供
resolveProjectTrust,先以未信任状态加载一次扩展,询问后再加载项目资源。 - 诊断集中返回 :服务创建不打印、不退出,而是返回
AgentSessionRuntimeDiagnostic[],由应用层决定展示方式。 - 失败尽量局部化:单个扩展加载失败、单个技能格式错误、单个 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 值得保持的不变量
- 项目资源必须在信任后加载。
- 资源冲突必须确定性解决并可诊断。
- 服务按
cwd重建,不跨目录复用。 - 单个资源/扩展/provider 失败不应拖垮整个会话。
- 模型请求的认证头和 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 统一装配的四类资源,其中只有扩展是可执行代码。