第 2 讲
Pi 项目全景:monorepo 结构与各 package 职责
拿到一个陌生的大型仓库,先别急着读代码——先画地图。本讲把 Pi 的 11 个核心 package 按依赖方向摆成一张分层图纸,之后每一讲你都能在图上找到自己的位置。
学习目标
- 能说出 Pi 仓库的顶层结构:
packages/、apps/、.pi/、scripts/各自装什么。 - 能脱稿复述 11 个核心 package 的职责一句话,并知道每个包的 npm 包名。
- 能画出依赖分层图:依赖只允许向下,
telemetry/schema在底、coding-agent在顶,并解释为什么。 - 能把根
package.json里npm run build的手工串联顺序与依赖图一一对应起来。 - 能解释
.pi/目录如何证明「Pi 用自身管理自身」(自我托管,self-hosting),并从AGENTS.md读出 Agent 时代的工程规范。
§2.1仓库顶层结构:先认楼,再进屋
读 monorepo 的第一件事不是打开源码,而是看目录。目录结构就是团队脑子里的架构图落在磁盘上的样子。
用 Get-ChildItem C:\pi_agent_study\pi-main 看根目录,除了 .github/、.husky/(Git 钩子)、biome.json、vitest.base.ts、tsconfig.base.json 这些工程配置外,真正承载架构的是四类目录:
| 目录 | 内容 | 本讲视角 |
|---|---|---|
packages/ | 11 个核心库 + 1 个评测工作区 evals(@earendil-works/pi-evals,被根脚本 npm run eval 引用) | 课程的主体,第 4~13 讲几乎都在这里面上课 |
apps/ | p2、p3 两个实验性应用(p2 有自己的插件实践文档与 Web UI) | chord/durable 方向的原型验证场 |
.pi/ | Pi 自己的配置目录:extensions/、skills/、prompts/、git/、npm/ | 「自我托管」的直接证据(§2.5) |
scripts/ | 构建与一致性检查脚本:biome-check.mjs、check-pinned-deps.mjs、check-runtime-deps.mjs、check-entry-graphs.mjs 等 | 工程规范的执行器(§2.6) |
还要记两个全仓库级事实:其一,根 package.json 声明 "type": "module",全部代码走 ESM;其二,engines 要求 node >= 22.19.0——Pi 明确依赖较新的 Node 运行时能力(后面 AGENTS.md 里的「erasable TypeScript / Node strip-only mode」也与此呼应)。
Java 视角 · 一
这套顶层结构等价于一个 Maven 多模块(multi-module)聚合工程:根目录就是只带 <modules> 和插件配置的父 POM,packages/* 是一个个 artifact 模块,apps/* 是可选的演示/实验模块,scripts/ 相当于你写在 CI 里的校验脚本(checkstyle、enforcer 规则)的仓库内版本。区别在于 npm 的模块边界由 package.json 声明,没有 Maven 那么强的「父 POM 继承」语义,所以 Pi 用脚本手工补齐了顺序与约束。
§2.211 个 package 职责一览:从零依赖底座到最终产品
先把每个包的一句话职责背下来,再谈分层。下表内容逐条对照各 packages/*/README.md 首屏与各 package.json 的 dependencies 核实过。
| package | npm 包名 | 一句话职责 | 内部依赖 |
|---|---|---|---|
telemetry | @earendil-works/pi-telemetry | 厂商中立的遥测契约与类型化 schema 工具:显式回调式 TelemetryContext/TelemetrySpan、NOOP_TELEMETRY_CONTEXT、内存参考实现;刻意不带 exporter、不设全局 current-span、不绑定任何后端 | 无 |
schema | @earendil-works/pi-schema | 无 eval 的 JSON Schema 构建器与校验器:schema 编译为普通闭包,支持 Draft 2020-12/7(官方核心测试 2222/2223 通过),错误用 JSON Pointer 路径定位,约 8.5 KB | 无 |
chord | @earendil-works/chord(注意:不带 pi- 前缀) | 跨进程对象运行时:对象类型即契约(JSON Schema 描述)、插件 facet、引用而非对象、文档/事务存储、插件热重载、HTTP/workers;是 durable 的地基 | schema、telemetry |
tui | @earendil-works/pi-tui | 终端 UI 库:差分渲染(differential rendering)只更新变化行、CSI 2026 同步输出防闪烁、bracketed paste、Text/Editor/Markdown/SelectList/ScrollView/Image 等组件体系、Kitty/iTerm2 内联图片 | 无 |
codemode | @earendil-works/pi-codemode | 在 QuickJS(编译为 WebAssembly)沙箱里运行模型编写的 JavaScript 脚本,唯一能力是调用注入的 tools;嵌套工具调用不进 LLM 上下文,只有脚本输出与返回值进入 | 无 |
mcp | @earendil-works/pi-mcp | 独立、轻量的 MCP(Model Context Protocol)客户端:transport 中立的核心 + Stdio/StreamableHttp/Pipe/内存测试传输;McpClient 负责 connect/listTools/callTool/close;不依赖官方 MCP SDK | 无 |
ai | @earendil-works/pi-ai | 统一多厂商 LLM API:provider 工厂集合、自动认证解析、token 与成本跟踪、流式、模型目录(chat 目录只收录支持 tool calling 的模型)、上下文持久化与模型间 hand-off | schema、telemetry |
durable | @earendil-works/pi-durable | 实验性持久化 Agent harness:会话、模型轮次、工具调用先落盘再展示,进程崩溃后重开存储可续跑;建在 pi-ai + chord 之上 | chord、ai、codemode、schema、telemetry |
env | @earendil-works/pi-env | Durable 的远程执行环境:远端跑一个 Rust 小守护进程(stdin/stdout 帧协议),RemoteExecutionEnv 经 SSH 驱动它;工具在远端执行而存储与凭据留在本地 | chord、durable |
agent | @earendil-works/pi-agent-core | Agent 运行时:有状态循环、工具执行、状态管理、事件流(如 agent.subscribe() 收 message_update 事件);建在 pi-ai 之上 | ai、schema |
coding-agent | @earendil-works/pi-coding-agent | 最终产品:交互式编码代理 CLI,组装 agent 内核 + tui 界面 + codemode/mcp 能力,并提供扩展/技能/提示模板/主题与 Pi Packages 生态 | agent、ai、codemode、mcp、tui |
三个容易看漏的细节,值得单独点出:
- telemetry 是「契约」不是「产品」。它只定义上下文接口、span 语义、度量 schema 与一个内存参考实现;接 OpenTelemetry、Sentry 还是纯日志,由应用侧写适配器。这是典型的「端口与适配器」思路——先中立,再接后端。
- 三个零内部依赖的「能力插件」:tui、codemode、mcp。它们不依赖任何兄弟包,可被任何项目单独
npm install。尤其mcp和codemode,README 明确写了可脱离 Pi 独立使用——独立成包不是目录洁癖,而是复用边界。 - chord → durable → env 是一条独立的实验线。注意
coding-agent的依赖列表里根本没有 chord/durable/env:CLI 主线与持久化实验线共享底座(schema/telemetry/ai),但互不纠缠。
Java 视角 · 二
pi-ai 对 pi-agent-core 的意义,等价于你把「调用外部服务的 SDK 抽象」和「业务编排引擎」拆成两个模块:ai 像 RestClient + 多厂商 SPI 实现(Anthropic/OpenAI/Google…各是一个 provider),agent 像拿这个 client 驱动业务状态机的服务层。而 telemetry/schema 之于全仓库,就是 slf4j-api 和参数校验器这类「谁都要用、谁都不许碰实现」的底座依赖。底座零依赖、能力库横向独立、编排层在中间、产品在顶——这和健康的多模块 Java 工程完全同构。
§2.3依赖方向与分层理由:为什么箭头只许朝下
把 §2.2 表格里的依赖关系画到图上,整个仓库的架构决策一目了然:依赖只允许从上往下指(上层用下层),任何反向或同层循环依赖都是结构错误。
package.json 的 dependencies 实测绘制)。虚线仅示意部分真实存在的重复依赖(如 schema→ai、ai→durable、chord→env、schema→agent);完整清单以各包 package.json 为准。注意 coding-agent 不依赖 chord/durable/env。为什么必须「只许向下」
- 构建顺序可推导。单向无环依赖图(DAG)天然存在拓扑排序,根目录那条手工串联的 build 命令就是它的展开(§2.4)。一旦出现循环依赖,构建顺序就只能靠碰运气。
- 底座可独立发布与替换。
telemetry不绑定后端、schema零依赖、mcp不依赖官方 SDK——底座越中立,上层换实现时震动越小。这与 Java 里「依赖接口模块、不依赖实现模块」是同一条纪律。 - 实验与主线隔离。durable 整条线标记 Experimental(README 明确 API 可在版本间无通知变更),通过「它依赖 ai/chord,但 coding-agent 不依赖它」把风险圈在分支内;主线 CLI 的发布节奏不被实验代码绑架。
- 复用边界即发布边界。能被外部项目单独使用的(tui、mcp、codemode、ai、agent-core…)都做成可发布的 npm 包;只有最终产品 coding-agent 是纯粹的「组装车间」。这个决策让 OpenClaw、pi-chat 这类第三方集成可以直接站在
pi-ai与 SDK 上,而不必拖走整个 CLI。
§2.4npm workspaces 与构建顺序:一条脚本里的拓扑排序
Pi 用 npm 原生 workspaces 管理 monorepo,没有引入 turbo/nx/lerna。它把「构建必须按依赖顺序」这件事写成了根 package.json 里一条手工串联的长命令——丑,但完全透明。
{
"name": "pi-monorepo",
"private": true,
"type": "module",
"workspaces": [
"packages/*",
"apps/*",
"packages/chord/test/examples/61-tutorial",
"packages/coding-agent/examples/extensions/with-deps",
"packages/coding-agent/examples/extensions/custom-provider-anthropic",
"packages/coding-agent/examples/extensions/gondolin"
]
}
注意 workspaces 不只覆盖 packages/* 与 apps/* 两个通配目录,还显式纳入了 4 个「嵌在工作区内部」的示例/测试项目——它们有自己的依赖需要安装。这正是 workspaces 的核心价值:所有子包共享一个根 node_modules 与一份 package-lock.json,内部依赖以 workspace 协议链接,装一次、全仓库可用。
"build": "cd packages/telemetry && npm run build &&
cd ../schema && npm run build &&
cd ../chord && npm run build &&
cd ../tui && npm run build &&
cd ../codemode && npm run build &&
cd ../mcp && npm run build &&
cd ../ai && npm run build &&
cd ../durable && npm run build &&
cd ../env && npm run build &&
cd ../agent && npm run build &&
cd ../coding-agent && npm run build"
同文件中还有 build:offline 变体:顺序完全相同,唯一差别是 packages/ai 执行 build:offline(离线构建模型数据),对应 Nix 离线打包场景。
把这条命令与图 2-1 对照,能读出三层信息:
- 硬约束:被依赖者先构建。
telemetry/schema最先(零依赖);chord、ai随后(只依赖底座);durable在chord/ai/codemode之后;env在durable之后;agent在ai之后;最后才轮到依赖五个包的coding-agent。这就是依赖图的拓扑排序(topological order)。 - 软自由:无依赖关系的包之间顺序任意。
chord与ai互不依赖,谁先都行——脚本把 chord 排在 ai 前,只是作者按「durable 实验线」归组的选择,不是依赖约束。同理tui/codemode/mcp三个零依赖库理论上可以提到最前面。所以看到构建顺序时,要能区分「必须如此」与「恰好如此」。 - 为什么不交给工具?npm 自带
--workspaces能按声明顺序遍历,但不保证按依赖拓扑排序(并发时更不保证);引入任务编排器又要加一层依赖。Pi 的选择是用一条显式长命令换取零工具依赖与完全可预测——极简哲学在工程决策上的延续。
Java 视角 · 三
Maven 的 reactor 会根据模块间依赖自动计算构建顺序,你不需要手写;Gradle 同理。Pi 这条手工 cd && npm run build 链相当于自己扮演 reactor——好处是任何人在任何机器上读一行脚本就知道全仓库构建全貌,坏处是新增包或调整依赖时要手动维护这条链。工程上这是典型的「确定性优先于自动化便利」取舍,CI 脚本里手写死顺序的 Java 项目也不少见。
与构建配套的还有根 check 脚本(节选字段名):biome-check、check:pinned-deps(依赖必须钉版本)、check:runtime-deps、check:ts-imports(禁止非法相对导入)、check:entry-graphs(入口图检查)、两遍 tsc --noEmit。测试侧则是 npm run test(scripts 单测 + 各包 vitest)与 test.sh、pi-test.sh/.ps1/.bat。这些名字在 §2.6 会再次出现。
§2.5.pi/ 目录:Pi 用自身管理自身
走进 .pi/,你会看到这个仓库最有趣的现象:Pi 的开发者不是「用别的工具开发 Pi」,而是「用 Pi 开发 Pi」。这叫自我托管(self-hosting / dogfooding),就像编译器用自己的语言写自己。
.pi/
├─ extensions/ # import-repro.ts · prompt-url-widget.ts · redraws.ts · tps.ts
├─ prompts/ # cl.md · deslop.md · dup.md · is.md · pr.md · sa.md · wr.md
├─ skills/ # add-llm-provider.md · github-work.md · interactive-testing.md
│ # · p2-ui.md · release.md
├─ git/ # 含 .gitignore
└─ npm/ # 含 .gitignore
对照 coding-agent 的文档体系解读三个子目录:
extensions/下的.ts文件是仓库自用的运行时扩展——例如tps.ts(每秒 token 数面板)、redraws.ts(重绘统计)、prompt-url-widget.ts。扩展机制本身就是 coding-agent 的核心特性,团队拿它调自己的产品。prompts/是提示模板:cl.md(changelog)、pr.md(PR 处理)、wr.md(代码评审)、deslop.md等,把「让 agent 干活的标准说法」固化成可复用资产。skills/是技能文件:release.md(发布流程)、github-work.md(GitHub 协作)、interactive-testing.md(tmux 测交互模式)、p2-ui.md(p2 Web UI 规范)——把「老员工脑子里的流程知识」写成 agent 可加载的操作手册。
而最硬的证据来自 AGENTS.md 本身,它直接把这些技能当依赖引用(原文节选):
Before writing or reviewing plugin code in apps/p2 ... read
apps/p2/docs/plugin-practices.md in full and follow it.
For building or changing p2's web UI (apps/p2), load and follow
.pi/skills/p2-ui.md.
For testing pi's interactive mode with tmux, load and follow
.pi/skills/interactive-testing.md.
For release preparation ... load and follow .pi/skills/release.md.
这段引用说明了自我托管的完整闭环:流程知识(release、测试、UI 规范)以 Pi 自己的 skills 格式存放,由 Pi 的 skill 机制按需加载,用来指导 AI 协作者开发 Pi 本身。如果这套机制不好用,第一个受害者就是 Pi 团队自己——这是最诚实的质量压力测试。同时注意 .pi/git/ 与 .pi/npm/ 里各有一个 .gitignore:自托管目录同样被当作普通工程资产做版本管理纪律,而非临时实验场。
Java 视角 · 四
类比理解:.pi/ 之于 Pi 仓库,相当于一个团队把自己内部的「CI 流程手册 + 代码评审 checklist + 常用脚本」从 Confluence 搬进了 IDE,只不过这些手册的「读者」不是新人,而是 AI 结对程序员。更准确的对应物是:把 CONTRIBUTING.md + 团队 Wiki + 脚手架模板合并成机器可执行、人可审阅的结构化资产。这也解释了为什么技能文件是 .md 而不是代码——它们既要给 agent 读,也要给人评审与迭代。
§2.6AGENTS.md:Agent 时代的工程规范长什么样
AGENTS.md 是根目录的一份「给 AI 协作者的开发规则」,与面向人类的 CONTRIBUTING.md 并列。它读起来像一份严格的 code review 意见合集——因为它的读者每天会产出成千上万行代码。
- Keep answers short and concise. No emojis. No fluff.
- Read files in full before wide-ranging changes ...
- No any unless absolutely necessary.
- Validate only at decided trust boundaries (wire input, schema-checked
arguments and params).
- **No inline imports** (await import() ...). Top-level imports only.
- Use only erasable TypeScript syntax (Node strip-only mode) in code checked
by the root config ...
- Never remove or downgrade code to fix type errors from outdated deps;
upgrade the dep instead.
- If the user's instructions conflict with any rule in this document, ask
for explicit confirmation before overriding.
逐条翻译成架构语言,这份文件在约束四件事:
- 可验证性。风格(biome)、类型(tsc)、测试(vitest +
test.sh)、依赖纪律(pinned deps、runtime deps、entry graphs)都有脚本兜底;AGENTS.md 里的大量规则其实能在scripts/下找到对应的检查器。规范不是品德要求,是 CI 断言。 - 信任边界(trust boundary)。「TypeScript 类型已保证的值不做运行时检查;只在 wire input 与 schema 校验过的参数处校验」——这与 Java 里「不要对内部调用做重复 null 检查、只在系统边界(Controller/外部 API)校验」是同一条原则,只是写得更极端。
- 确定性优先。禁止内联动态导入、只用可擦除(erasable)TypeScript 语法以配合 Node strip-only 模式、不许靠降级代码绕开类型错误——这些都是在压缩 agent 与工具链的「解释空间」,让静态分析与直接运行的结果可预测。
- 流程与权限。Changelog 逐包维护、
[Unreleased]区不可篡改已发布段、PR 不得随意 checkout、用户指令与规范冲突时必须先确认再覆盖——最后这条本质是给 AI 协作者划了「变更需授权」的红线。
把这些放回 §2.5 的语境:Pi 把「AI 是一等贡献者」当作既定事实,然后围绕它重建工程规范——规范的主要读者从「新人 + reviewer」变成「agent + reviewer」,且规范本身用 Pi 的 skills 机制分发。这就是「Agent 时代的工程规范」样本:不是多写一份文档,而是静态检查可执行化、流程知识结构化、信任边界显式化、变更授权明确化。
动手实践
目标:离开本页前,亲手验证一遍本讲的每个论断。以下命令均在 PowerShell 中针对本地仓库执行。
- 枚举顶层与包目录:
Get-ChildItem C:\pi_agent_study\pi-main | Select Name与Get-ChildItem C:\pi_agent_study\pi-main\packages | Select Name。确认 11 个核心包 +evals,再列出apps\与.pi\的子目录。 - 读首屏建索引:依次打开 11 个
packages\*\README.md,只读第一屏(标题 + 第一段 + 特性列表),给每个包写一行自己的话——写不出来就回去重读 §2.2 的表。 - 核实依赖方向:对每个包执行
(Get-Content packages\agent\package.json -Raw | ConvertFrom-Json).dependencies(替换包名),记录所有@earendil-works/*依赖,与图 2-1 对照,标出你漏画或画错的边。 - 验证构建顺序:打开根
package.json,把 build 脚本里 11 个cd目标抄成有序列表,逐一检查「它依赖的包是否都排在前面」;再解释哪几对相邻包其实可以互换顺序。 - 看自我托管:打开
.pi\skills\release.md的开头,再在AGENTS.md里搜索对它的引用;用一句话写下「这条引用如何构成自我托管闭环」。
附常见误区
McpClient 负责请求关联、初始化、超时与取消。独立实现的动机是轻量、可控、可单独复用。
pi-ai 统一抽象(provider、认证、流式、模型目录);coding-agent 依赖 pi-agent-core 与 pi-ai,自己不做协议适配。这也是它能在多厂商间切换而主体代码不动的原因。
durable 的 README 标注 Experimental,env 服务于 durable 的远程执行;而 coding-agent 的依赖列表里没有这三者。把实验线当成主线代码去精读,会在课程后半程浪费大量时间。
chord 与 ai 互不依赖,先后无所谓;tui/codemode/mcp 零内部依赖,理论上也能提前。读构建脚本要区分硬约束与作者的选择。
附自测问答
问题 1 · pi-agent-core 与 pi-ai 的边界在哪?
pi-ai 负责「一次模型请求」:provider 适配、认证解析、消息与流式事件、模型目录、token 与成本统计——它是无状态偏底座的统一 LLM API。pi-agent-core 负责「跨多次请求的循环与状态」:驱动「请求 → 工具执行 → 结果回填 → 再请求」,维护会话状态并以事件流(subscribe / message_update 等)对外播报。证据是依赖方向:agent 依赖 ai 与 schema,反向不存在。类比 Java:ai 是带多实现的 SDK 抽象层,agent 是用它编排业务的服务层。
问题 2 · 为什么 MCP 客户端要独立成包,且不依赖官方 SDK?
三个理由:① 复用边界——mcp 零 pi 依赖,任何项目都可单独 npm install @earendil-works/pi-mcp 暴露自己的能力;② 依赖面控制——不拖官方 SDK 的传递依赖,符合仓库 pinned-deps / runtime-deps 的依赖纪律,也让 CLI 安装体积与供应链风险更小;③ 协议可控——自实现 transport 抽象(Stdio/StreamableHttp/Pipe/内存测试)后,客户端核心与 I/O 彻底分离,测试可以用内存 transport,超时、取消、请求关联的行为完全由自己定义。
问题 3 · .pi/ 目录为什么能证明「Pi 可自我托管」?
因为存放其中的资产类型——extensions、prompts、skills——全部是 Pi 自身提供的扩展机制,且被用于开发 Pi 仓库本身:prompts/pr.md、skills/release.md 等在 AGENTS.md 中被显式引用为协作流程的一部分(如 "load and follow .pi/skills/release.md")。也就是说:流程知识用 Pi 的格式写、由 Pi 的机制加载、服务于 Pi 的开发——形式与用途闭环,即 self-hosting / dogfooding。编译器界的老话:能自己编译自己的编译器才算成熟。
问题 4 · npm run build 的手工串联顺序说明了什么?随意打乱会怎样?
它是依赖 DAG 的一个拓扑排序:被依赖者必须先构建(telemetry/schema 最前、coding-agent 最后)。打乱的后果分两种:把某包排到它的依赖之前,构建会因解析不到 workspace 内产物而失败(硬约束);调换互不依赖的包(如 chord 与 ai、三个零依赖库与其他包),则完全无害(软自由)。这个顺序是作者显式维护而非工具推导——体现「确定性优先于自动化便利」的取舍。
问题 5 · pi-env 服务于哪条产品线?它为什么排在 agent 之前构建?
服务于 durable 实验线而非主线 CLI:env 依赖 chord 与 durable,为 Durable 提供 RemoteExecutionEnv(远端 Rust daemon + SSH),让工具在远端机器执行而存储与凭据留在本地;coding-agent 不依赖它。排在 agent 之前只是脚本作者的分组选择(先走完 durable 分支再走 agent 与 coding-agent 主线),二者互不依赖,顺序可互换。
Pi Agent 源码学习课程 · 第 2 讲 · 内容基于本地源码 C:\pi_agent_study\pi-main(根 README、package.json、11 个包 README 首屏、AGENTS.md)实测核对。