PiAgent 02|读懂包分层与会话装配#
从
pi-ai到pi-coding-agent,沿依赖方向理解模型、循环、工具、会话与 UI 为什么没有混在一起
打开 Pi 的 monorepo,最容易得到的错觉是:它只是一个功能很多的 CLI。更准确的理解是,它把“模型协议”“Agent 控制流”“编码产品装配”和“终端界面”放在了不同边界内。包的数量不是重点,重点是每层拥有的状态不同、变化速度不同、被谁依赖不同。
1. 先看依赖方向#
可以先用下面的简化图建立空间感:
pi-coding-agent
┌──────────┼──────────┐
│ │ │
AgentSession extensions pi-tui
│
pi-agent-core
│
pi-ai这不是严格的包依赖图,而是职责图:
pi-ai只关心模型、上下文、消息和流式 Provider,不应该知道会话文件或终端控件。pi-agent-core负责 Agent 状态与循环,可以被非编码场景复用。pi-coding-agent把模型、资源、设置、会话、默认工具和扩展装配成产品级 Session。pi-tui负责把事件投影成人可以操作的终端界面。
依赖方向一旦倒置,问题会变得难以隔离。例如模型适配器直接引用 TUI,就无法在 Web 服务或测试中复用;工具把会话文件路径写死,就无法在多用户环境安全运行。
2. pi-ai:统一协议,而不是抹平能力差异#
上层希望调用一个统一的 stream(model, context),但 Provider 的现实差异很多:消息角色名称不同,工具参数的 JSON 约束不同,thinking、缓存、停止原因和错误码也不一致。
const model = getModel(provider, modelId);
const context = { systemPrompt, messages };
for await (const event of stream(model, context)) {
if (event.type === "text_delta") {
process.stdout.write(event.delta);
}
}适配层做两次转换:
- 把 Pi 的上下文模型转换成具体 Provider 的请求格式。
- 把 Provider 的响应转换成统一的文本、thinking、tool call、usage 和错误事件。
统一接口的含义是上层拥有稳定的最低公约数,不是所有模型都拥有相同能力。某个模型没有 thinking 或不支持工具时,适配器不能凭空创造能力,只能用能力字段和错误协议让上层知道差异。
3. pi-agent-core:把模型输出变成控制流#
核心 Agent 不负责“这个产品应该显示什么”,它只关心一件事:当前消息和工具状态如何驱动下一次模型调用。
这层的价值在于把 ReAct 之类的控制流从业务代码中抽出来。业务代码不需要手写“如果模型返回 tool call 就再请求一次”,而是向 Agent 提供工具和规则,由 Loop 持续推进直到达到终止条件。
4. pi-coding-agent:产品层的装配器#
pi-coding-agent 不是另一个更大的“模型类”,它是把多个运行时组件接到一起的外壳。最小装配可以理解为:
ModelRuntime 选择模型与认证
SettingsManager 加载默认设置、重试与压缩策略
SessionManager 保存或恢复会话树
ResourceLoader 发现系统提示词、skills、extensions、上下文文件
ExtensionRuntime 注册工具、命令和生命周期钩子
Agent 执行循环、消息与工具调用
AgentSession 对外暴露 prompt/subscribe/abort 等产品接口因此 createAgentSession() 的意义不是“new 一个 Agent”,而是完成依赖注入。它把这些组件的默认实现组合起来,同时给宿主留下替换入口。
5. 从一个调用看五步装配#
const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({
model: (await modelRuntime.getAvailable())[0],
modelRuntime,
});在默认配置下,可以把内部流程理解为:
1. ModelRuntime:读 auth.json / models.json,合并内置模型
2. SettingsManager:读 settings.json,获得默认设置
3. SessionManager:决定会话写入 JSONL 还是留在内存
4. ResourceLoader:加载系统提示词、skills、extensions、AGENTS.md
5. Agent:把模型、消息、工具和事件循环接起来默认 Coding Agent 通常启用 read、bash、edit、write 四个核心工具;其他内置工具可能需要显式加入。源码里能找到某个工具,并不代表新建的 Session 一定把它暴露给模型。
6. Session、Runtime、Extension 和 Tool 的关系#
Session:持续对话的容器#
Session 持有当前对话和生命周期入口。prompt() 触发运行,subscribe() 观察运行,abort() 中止运行,dispose() 释放资源。它不应该承担 Provider 适配,也不应该把 Web 的用户身份直接写进每条模型消息。
Runtime:Session 背后的基础设施#
Runtime 解决的是“Session 如何工作”:模型从哪里来、资源从哪里加载、历史怎么保存、设置如何生效。替换 Runtime 组件,就是在改变产品的运行环境,而不必重写 Agent Loop。
Extension:运行时的策略注入点#
扩展可以注册模型可调用的工具,也可以订阅或拦截生命周期事件,还可以提供用户显式触发的命令。它位于产品层和核心循环之间,因此既能观察事实,也可能改变事实。
Tool:模型触达外部世界的边界#
工具是模型能力的物化契约。没有查询工具,模型就不能可靠获得数据库事实;有了没有权限控制的写入工具,模型就可能把自然语言判断转成危险副作用。
7. 为什么 Web 场景不能照搬默认装配#
默认会话持久化是为本地、单用户、按工作目录隔离的 Coding Agent 设计的。它通常把 JSONL 写入 ~/.pi/agent/sessions/<encoded-cwd>/。
Web 服务的约束不同:
- 多个用户不能共享一个 Session。
- 会话历史应该按用户和会话 ID 存入受控存储,而不是任意写服务器磁盘。
- 系统提示词和工具权限不能由不可信工作目录随意决定。
- 一个请求断开时,必须只中止对应运行,不能影响其他用户。
因此常见做法是使用 SessionManager.inMemory() 作为运行时容器,然后通过事件或消息边界把需要恢复的事实写入应用自己的数据库。这个选择不是性能优化,而是把“本地 CLI 的默认假设”与“Web 产品的多租户边界”分开。
8. 源码阅读的边界判断法#
看一个符号时,可以连续问四个问题:
- 它拥有的是模型状态、Agent 状态、会话状态还是 UI 状态?
- 它接收的是内部消息,还是已经可以发送给 LLM 的消息?
- 它是在改变运行策略,还是只观察运行结果?
- 它的默认实现是否针对本地单用户场景?
例如 session.subscribe() 是观察入口,不等于扩展的 pi.on();SessionManager 管理可恢复历史,不等于 Agent 的 transcript;ToolDefinition 描述模型能调用什么,不等于工具执行结果已经安全。
小结
Pi 的包分层可以浓缩成一句话:pi-ai 处理模型方言,pi-agent-core 处理行动循环,pi-coding-agent 处理产品装配,TUI 或 Web 负责事件投影。理解这四个边界后,改模型只需要找适配器,改提示词只需要找资源加载,改工具只需要进入扩展或工具契约,不必把整套 CLI 拆开重写。
源码定位
- 模型类型与事件:
packages/ai/src/types.ts - Agent 核心类型与循环:
packages/agent/src/types.ts、packages/agent/src/agent-loop.ts - Session 装配:
packages/coding-agent/src/sdk.ts - Session 生命周期:
packages/coding-agent/src/agent-session.ts - 默认工具:
packages/coding-agent/src/core/tools/ - 终端投影:
packages/tui/