跳到主内容
创建时间:最后更新:作者:白川

版本与源码地图:定位智能体的分层架构

把 Pi 的仓库结构、包清单和分层架构一次讲清,并给出记录当前源码版本、区分安装版本与仓库版本的具体办法。

版本与源码地图:定位智能体的分层架构

想读 Pi 的源码,第一步就会卡住:你读的是哪一版?

Pi 的 monorepo 迭代很快,main 分支随时在动,今天看到的目录结构和下周的可能就不一样。更麻烦的是,你机器上装的那一版和仓库 main 分支不是同一个东西——pi --version 显示的是前者,你打开源码文件看到的是后者。搞混这两个,后面所有"文档说支持 X"的判断都不可靠。

这篇解决两件事:把 Pi 的仓库结构、包清单和分层架构一次讲清,让你知道每一层的源码在哪、归哪个包发布;然后给出一套记录当前源码版本的具体办法,让你的阅读结论可复核。

读完你会有一份自己的版本地图笔记:一个提交号,一份包清单,一张分层图。

读这篇需要的前置只有一条:会用 git 的基本命令(rev-parse、describe、diff、log)。

一、仓库在哪,scope 是什么

Pi 的官方仓库是 https://github.com/earendil-works/pi。这是一个 monorepo,仓库根目录下的 packages 目录里,每个子目录是一个独立发布的 npm 包。

仓库本身经历过迁移,这一条值得先记清楚:

事实说明
仓库地址
github.com/earendil-works/pi
npm scope
@earendil-works/*
旧 scope
@mariozechner/*,已迁移
主分支
main
发布 tag
v1.x.y,例如 v1.3.0

旧 scope 的迁移在官方 CHANGELOG 里有明确记录:

Self-update support for the npm scope migration: pi update --self now supports the upcoming package rename from @mariozechner/pi-coding-agent to @earendil-works/pi-coding-agent.

这一条对你的直接影响是:看到 @mariozechner/pi-* 的教程或代码,那是很早的版本。 现在应该用的全部是 @earendil-works/*。

包清单

当前 packages 目录下的子目录与它们对应的 npm 包:

目录npm 包名一句话职责
ai
@earendil-works/pi-ai
统一的多 provider LLM 接口:模型目录、认证解析、流式事件、用量统计
agent
@earendil-works/pi-agent-core
Agent 运行时:工具执行循环、事件流、队列、beforeToolCall / afterToolCall / finishTurn
coding-agent
@earendil-works/pi-coding-agent
编码智能体 CLI 与 SDK:内置工具、会话管理、扩展系统、资源发现
codemode
@earendil-works/pi-codemode
QuickJS 沙箱里跑模型写的 JavaScript,唯一能力是调用注入的工具
mcp
@earendil-works/pi-mcp
独立的 MCP 客户端,不依赖官方 MCP SDK,也不依赖其它 pi 包
tui
@earendil-works/pi-tui
终端 UI 框架:差分渲染、同步输出、防闪烁
durable
@earendil-works/pi-durable
持久化的对话、任务和文档运行时。官方标注实验性,API 会随版本变动
env
@earendil-works/pi-env
给 pi-durable 用的远程执行环境:SSH 部署的小守护进程
chord
@earendil-works/chord
跨进程对象的类型化宿主运行时:热重载、JSON 链路上的 RPC
telemetry
@earendil-works/pi-telemetry
与厂商无关的遥测契约和类型化 schema,无 exporter、无全局状态
evals
@earendil-works/pi-evals
基于 vitest 的行为评估

这张清单本身就回答了"该装哪个包":

包职责什么时候该装它
pi-ai
统一模型接口
只要调模型就要装。provider 从子路径导入
pi-agent-core
工具循环与事件流
你要自建智能体循环,且不需要终端和会话文件
pi-coding-agent
完整的编码智能体
要 CLI、内置编码工具、会话树、扩展系统
pi-codemode
沙箱脚本执行
要让模型用脚本批量编排工具调用
pi-mcp
MCP 客户端
要接 MCP server,但不想引入整个 Coding Agent
pi-tui
终端 UI
要写自己的终端界面
pi-durable
持久化运行时
要跨重启恢复工作;接受它的 API 会变
pi-env
远程执行环境
用 pi-durable 且工具要跑在另一台机器上
chord
跨进程对象
要把对象放进多个进程
pi-telemetry
遥测契约
要接 OpenTelemetry / Sentry 之类后端
pi-evals
行为评估
要给 Pi 写回归评估

二、分层架构

Pi 的主干是三层,箭头表示依赖方向:

text
# Pi 的分层架构,三层主干在上,可选服务层在下
┌──────────────────────────────────────────────────┐
│  @earendil-works/pi-coding-agent                 │
│  CLI + SDK、内置编码工具、会话树、扩展系统、资源发现   │
├──────────────────────────────────────────────────┤
│  @earendil-works/pi-agent-core                   │
│  工具执行循环、事件流、队列、上下文转换              │
├──────────────────────────────────────────────────┤
│  @earendil-works/pi-ai                           │
│  模型目录、provider 接入、认证解析、流式接口、用量统计  │
└──────────────────────────────────────────────────┘
        ↑ 叶子包,不依赖上面任何一层
┌──────────────────────────────────────────────────┐
│  pi-codemode · pi-mcp · pi-tui · pi-telemetry      │
│  pi-durable · pi-env · chord · pi-evals           │
└──────────────────────────────────────────────────┘

下层不知道上层。pi-ai 里没有"智能体"这个概念,pi-agent-core 里没有终端也没有会话文件。

每一层的源码在哪

读源码时按这个顺序定位,能少走很多弯路:

你要理解的东西先看哪里
模型怎么调、流式事件长什么样
模型层说明 的 Tools / Streaming 章节
Agent 循环怎么转、工具怎么执行
事件类型和钩子契约
会话怎么存、怎么恢复
工具怎么定义、扩展怎么写
CLI 参数和配置项
MCP 接入细节
沙箱怎么实现

可选服务层

服务层的包(pi-durable、pi-env、chord、pi-telemetry、pi-evals)是可选的,不在主干依赖链上。

包依赖方向定位
pi-durable
依赖 chord、pi-ai、pi-codemode
会话、模型轮次、工具调用在展示之前先提交到存储;进程崩了能接着跑
pi-env
依赖 chord、pi-durable
工具跑在远程机器(通常 SSH),存储和凭据留在本地
pi-evals
依赖 pi-ai、pi-coding-agent
给 Pi 写行为评估
chord
无 pi 依赖
把对象放到多个进程里,支持热重载
pi-telemetry
无 pi 依赖
只定义契约,不带 exporter,不带全局状态

注意 pi-telemetry 的取向:遥测上下文是显式传递的。这是为了让 pi 包之间的遥测不互相污染。

依赖关系(来自各包 package.json)

包直接依赖的 pi 包
pi-ai
pi-telemetry
pi-agent-core
pi-ai
pi-coding-agent
pi-agent-core、pi-ai、pi-codemode、pi-mcp、pi-tui
pi-durable
chord、pi-ai、pi-codemode
pi-env
chord、pi-durable
pi-evals
pi-ai、pi-coding-agent
pi-mcp / pi-codemode / pi-tui / pi-telemetry / chord
无 pi 依赖

这张表解释了一个事实:装 pi-coding-agent 会自动带来 pi-agent-core、pi-ai、pi-codemode、pi-mcp、pi-tui。 你没法只要 Coding Agent 而不要 MCP 客户端。

三、怎么记录你读到的是哪一版

这是本篇最实用的一节。

记录提交号

bash
# 进入你的 checkout 目录,命令要在仓库里跑
cd /path/to/pi
# 输出当前 HEAD 的 40 位 SHA-1,作为阅读结论的复核坐标
git rev-parse HEAD

输出是一个 40 位的 SHA-1。把这个 SHA-1 写进你的笔记。 以后任何人问你"你看的是哪一版",你给一个提交号,对方就能精确复核;给一句"最新版"则什么都对不上。

想顺便知道这个提交在哪个 tag 之后:

bash
# 看当前提交距离最近发布 tag 有多远,形如 v1.3.0-148-g8efd314fa
git describe --tags

输出形如 v1.3.0-148-g8efd314fa,意思是"在 v1.3.0 之后 148 个提交,当前提交是 8efd314fa"。v1.3.0-148 表示这还没发布——它比最近的 tag 新 148 个提交。

注意这个提交号不是你写的笔记的一部分要公开传播的东西。它是你自己复核用的坐标,公开引用时用 main 分支链接即可。

锁定坐标和"最新"不是一回事

GitHub 上定位到某个提交时,URL 里的分支名会被替换成一个提交号:

text
https://github.com/earendil-works/pi/blob/<提交号>/<文件路径>

把 URL 里的 main 换成那个提交号,链接就锁死了。但文章正文里不要这样写链接——读者拿到的永远是某个具体提交号之外的活代码,写死快照号既不准确也不能点击。写文章、做分享时,指向 main 分支的链接更合适;只有在你自己排查问题时才需要那个精确坐标。

需要对照你的记录和当前 main 的差异时:

bash
# 拉取远端最新状态,拿到能对比的当前 HEAD
git fetch origin
# 对比你记录的提交号和本地 HEAD,--stat 先给变更文件清单
git diff <你的提交号> HEAD --stat

--stat 先给你一个变更文件清单。确定要看哪几个目录后,再把目录作为路径过滤参数追加到 -- 后面,范围就限制住了。

一个容易踩的坑

本地工作树的 HEAD 未必等于你记录的提交号。 如果你 git pull 过,或者仓库被别人更新过,本地文件就比记录的版本新了。上面那条 git diff 就是用来发现这件事的。

反过来也成立:本地可能是你所记录版本的祖先,此时 git diff 会显示记录里有本地没有的内容。两种情况都要看一眼 diff 再引用。

四、安装版本和 main 分支是两件事

pi --version 输出的是你装的那一版。它来自包自己的 package.json:

typescript
// 编码智能体包的配置模块
export const VERSION: string = pkg.version || "0.0.0";

而仓库 main 分支上的 package.json 里的 version 字段,可能是下一个还没发布的版本号。

你看到的它代表什么
pi --version
你机器上那个 npm 包的版本
仓库 package.json 的 version
main 分支准备发布的下一个版本
git describe --tags
当前提交距离最近发布 tag 有多远
git rev-parse HEAD
唯一精确的坐标

这四个数不一样,是正常的。 关键是知道自己在用哪一个。

安装渠道的差别也不小

安装方式依赖锁定升级方式
官方安装脚本(curl ... install.sh)
锁定全部依赖
pi update
npm 全局安装(--ignore-scripts)
不锁传递依赖
npm update -g 或重装
Nix
用 pi.dev 模型目录的固定 revision
nix profile upgrade pi,pi update 管不了
源码 checkout
仓库自己的 package-lock.json
git pull + npm run build

第一条尤其重要。官方安装器装的 Pi,传递依赖是锁死的,所以你 pi --version 看到的版本,它跑出来的行为就是那个版本的行为。用 npm 装的则不然——同一个 pi --version,底层依赖可能不一样。

Pi 官方仓库自己在 README 里的开发流程是:

bash
# 官方仓库的开发流程,在 checkout 根目录按顺序执行
# 装全部依赖:Pi 正常安装不需要跑依赖的生命周期脚本,所以必须加 --ignore-scripts
npm install --ignore-scripts
# 构建所有包:先刷新模型数据再构建,这一步需要联网
npm run build
# 用现有模型数据重建,不访问网络:网络不通时用这个
npm run build:offline
# lint、格式化、类型检查三件事一起做:提交前跑一遍
npm run check
# 跑测试:没有 API key 时,依赖 LLM 的那部分会自动跳过
./test.sh
# 从源码跑 pi:想在任意目录验证改动时用它,而不是 npm 上装的那一版
./pi-test.sh

./pi-test.sh 是读源码时最实用的入口——它跑的是你 checkout 里的代码,不是 npm 上装的那一版。

供应链加固

官方 README 有一节讲这件事,值得知道,因为它影响你"能不能复现":

Direct external dependencies are pinned to exact versions. Internal workspace packages remain version-ranged.

package-lock.json is the dependency ground truth. Pre-commit blocks accidental lockfile commits unless PI_ALLOW_LOCKFILE_CHANGE=1 is set.

package-lock.json 是依赖的权威事实来源。 如果你要精确复现某个版本的运行环境,看这个文件,不要看 package.json 里的版本范围。

五、一份可复核的阅读基线

把上面的方法落成一份笔记模板:

markdown
# Pi 阅读基线

- 仓库:https://github.com/earendil-works/pi
- 我的提交号:<填 git rev-parse HEAD 的输出>
- git describe:<填 git describe --tags 的输出>
- 仓库包版本:<填各包 package.json 里的 version>
- 我本机装的:pi --version = <填这里>

## 已核对的文件

| 文件 | 用途 |
| --- | --- |
| Codemode 官方文档 | 脚本 API 与沙箱限制 |
| MCP 官方文档 | MCP 接入与曝光策略 |

## 已知差异

- 本地 HEAD 与我记录的提交的差异:<git diff 的结果>

最后一行是关键。记下差异,你就知道自己哪些结论是基于某个固定版本、哪些是基于本地代码。

六、官方已定义、源码事实与教学设计

官方已定义的行为:仓库地址 github.com/earendil-works/pi;npm scope @earendil-works/*;各包的职责与依赖方向;pi-ai 的 chat 目录只收支持工具调用的模型;pi-durable 是实验性包,API 会随版本变动;官方安装器锁定全部依赖,npm 安装不锁;package-lock.json 是依赖的权威事实来源;git describe --tags 输出中的数字表示领先最近 tag 多少个提交。

源码事实但官方文档没单独成段的:VERSION 常量取自 package.json 的 version 字段,缺失时回退 "0.0.0";pi-durable 依赖 chord、pi-ai、pi-codemode 三项;pi-env 依赖 chord 和 pi-durable;pi-evals 依赖 pi-ai 和 pi-coding-agent;chord 与 pi-telemetry 都对 pi 包零依赖。

教学设计:这张"包 → 职责 → 何时该装"表;这份阅读基线笔记模板;把 git diff 作为"差异检查"的固定动作——官方没有发布过阅读基线的标准格式。

你现在能做什么

  • 用 git rev-parse HEAD 和 git describe --tags 记录你当前读到的源码坐标,并写进一份自己的阅读基线笔记。
  • 说出 Pi 的模型层、Agent Core、编码智能体和可选服务层各对应哪个目录、由哪个 npm 包发布,共 11 个包一个不漏。
  • 解释为什么 pi --version 的输出不能代表你正在读的源码,并画出四条"你看到的版本号各自代表什么"的对照。
  • 查各包的 package.json,验证"装 pi-coding-agent 会连带装进哪五个包"这个结论。
  • 在你的机器上跑 pi --version,并和仓库 main 分支上的包版本对比,回答"我装的那版落后几个版本"。
  • 用 git diff <提交号> HEAD -- <目录> 发现工作树是否已经比你的记录更新。
  • 跑通 ./pi-test.sh,确认你执行的是 checkout 里的代码而不是 npm 上装的那一版。
  • 区分一段结论的来源:官方文档写明的、源码里读到的、还是你自己为了教学构造的。

排障速查

现象检查点处理
文档说的功能我这儿没有
版本
文档写的和你装的可能不是一版;用 git diff 对照
pi --version 和文档写的对不上
两者含义不同
前者是安装版本,后者是仓库当前状态;用 git rev-parse HEAD 定位源码
找不到某个包
scope 变了
现在是 @earendil-works/*;@mariozechner/* 是旧 scope
依赖版本对不上
看错文件了
package-lock.json 才是权威,package.json 里是版本范围
npm 装的 Pi 行为和官方脚本装的不一样
依赖锁定
官方安装器锁全部传递依赖,npm 不锁
Nix 装的 Pi 升不了
包管理器冲突
pi update 不能更新 Nix 安装;用 nix profile upgrade pi
本地代码和记录不一致
工作树被更新过
git diff <提交号> HEAD -- <目录> 看差异
想跑源码而不是装的版本
二进制来源
用 ./pi-test.sh,它跑 checkout 里的代码
改了源码没效果
没重新构建
npm run build 之后再跑
装 pi-coding-agent 却带出一堆包
依赖方向
pi-coding-agent 依赖 pi-agent-core、pi-ai、pi-codemode、pi-mcp、pi-tui

参考资料