Skip to content

Part II · 源码篇 / Ch 01

源码篇第1章 地图的比例尺——pi-mono ​

小A和老Z俯看分层地图,用放大镜和空白尺聚焦一条橙色主路径,穿过溪流、环形小路与小屋。

小A读完概念篇,信心满满地打开了 pi-mono。三分钟后,他盯着屏幕发呆:packages/ 下面躺着十几个目录,ai、agent、tui、coding-agent、protocol……他随手点开一个,又是几百个文件。

“老Z,”他喊道,“这地图也太大了。我该从哪个目录开始?”

老Z走过来,看了一眼屏幕,反问:“概念篇留下的问题是什么?”

“模型、循环、会话和终端各自的边界,在一套真实工程里究竟落在哪里。”小A背得很熟。

“对。你不是来‘读完 Pi’的,是来验证这几个边界的。”老Z说,“所以先别按目录猜,先固定地图的比例尺,再画主路径。”

本篇的已确认事实全部以上游 pi 仓库(github.com/earendil-works/pi,2026-09 由 badlogic/pi-mono 迁移而来)的标签 v1.0.0 为准,提交 a13d35a742c6ef8462812a28fbe1d8c8b7431c32,提交日期 2026-10-01。本书沿用 pi-mono 这个名字指这个 monorepo。下列八个包的 package.json 版本均为 1.0.0,而根 package.json 的 monorepo 版本是 0.0.3,不能混用。本书仓库的 pi/ 目录是这个标签的快照,另加了中文注释,路径和符号与上游一致,行号会因注释而偏移。

这个“八个”是本书的阅读范围,不是根 workspace 的总数:packages/ 下共有 13 个包,另外五个是 chord、codemode、durable、mcp、telemetry。chord 与 telemetry 只在用到时点名;codemode 与 mcp 在附录10、附录11 细读;durable 是源码篇第13章的专章对象。根 workspace 还包含扩展示例;packages/evals 是私有工作区包,不是发布包。

Pi 1.0 带来的七项产品变化 ​

小A问:“既然源码基线是 v1.0.0,这一版给使用者增加了什么?”

老Z说:“先看 pi-coding-agent 的 1.0.0 发布说明。它把七项产品变化列在 New Features 下。这里总结的是 1.0 发布时公布的亮点,不表示每项底层能力都在 1.0 才首次出现;例如 Codemode 和 MCP 在更早的版本里已经出现,1.0 继续改进了它们。”

1.0 发布亮点具体变化继续阅读
全屏终端界面交互界面默认使用全屏模式;设 tuiMode: "regular" 可恢复普通终端滚屏。源码篇第6章 终端;终端设置
更精简的 Codemode发布说明称默认工具配置下提示词约减少 40%,并改进错误信息,让模型能看到缺失名称的近似项和修复方向。这个比例是发布说明给出的概述,不是所有模型、工具组合都保证的固定值。Codemode 文档
在 Codemode 中生成图像脚本可通过 models.generateImages() 调用图像模型,使用当前会话凭据;生成结果可由 image() 交回模型。Codemode 图像生成
Radius 登录/login 可登录 Radius,并接着配置它提供的 MCP server。Provider 文档
Anthropic 复制授权码登录浏览器在另一台机器上时,可从授权页面复制代码并粘贴回 Pi,适用于远程或无头环境。交互式认证
加固 MCP OAuth可指定授权服务器元数据地址;按 RFC 9207 校验授权响应中的 iss;凭据按 server 名称和 URL 区分;追加权限时保留已授予的 scope。MCP OAuth 文档
只显示启动标题quietStartup: "header" 保留版本号和快捷键提示,同时隐藏模型范围与已加载资源列表。终端设置

“这七项主要改变交互、模型能力接入和认证体验。”老Z说,“架构层还有一项需要单独记:@earendil-works/pi-durable 在 1.0 首次发布;同一版把实验性 harness 从 pi-agent-core 移除,作为破坏性 API 变更,将耐久会话能力交给 pi-durable。这是包边界调整,不是上面七项 CLI 功能之一,也不表示 Pi 不再支持耐久会话。”(durable 发布记录;agent-core 变更记录)

源码篇后续会在源码篇第5章、源码篇第6章和源码篇第8章解释会话、终端与扩展边界。本节先给出版本地图;要核对完整范围和每条说明,应回到固定标签 v1.0.0 的各包 changelog 与文档。

先固定地图的比例尺 ​

小A:“八个包……是不是八个独立程序?”

“不是。”老Z说,“工作区名和发布包边界能证明可独立导入的单元,不能证明运行时一定独立进程。先读包元数据和导出,再顺着一次输入的控制流找证据。”

比喻:地图的比例尺

先定“一厘米代表多少公里”,地图才不误导。八个包是本书的阅读比例尺,不是仓库的全貌,更不是八个独立进程。

本书所说“八个包”是下表选取的八个产品/基础范围包。包的职责来自其 package.json 的名称、描述、依赖和导出;描述是维护者声明,不是对全部代码行为的证明。

包npm 包名可确认的边界证据本书沿它追什么暂不在本章断言
packages/ai@earendil-works/pi-ai有根、api/*、providers/* 等导出模型、认证、流适配各厂商服务质量
packages/agent@earendil-works/pi-agent-core只依赖 pi-ai,只有根导出Agent 与循环会话存储(已不在本包)
packages/tui@earendil-works/pi-tui零内部依赖的终端 UI 包组件与渲染Agent 决策
packages/coding-agent@earendil-works/pi-coding-agentpi 二进制入口,依赖 9 个内部包命令行装配、会话、压缩、工具与扩展所有扩展行为
packages/protocol@earendil-works/pi-protocol传输中立的 CBOR 协议包,依赖 chord帧、信封 schema、版本网络可靠性
packages/client@earendil-works/pi-client依赖 chord、pi-protocol远程客户端连接与请求服务端策略
packages/server@earendil-works/pi-server描述为 experimental,依赖 chord、pi-protocol握手、请求路由、会话附着生产部署保证
packages/evals@earendil-works/pi-evals私有评测工作区包harness、对照实验与 judge产品质量结论

小A的目光停在 coding-agent 那行:“它依赖九个内部包?那 agent 呢?”

“agent 只直接依赖 AI。”老Z说,“这也是这张表里最容易跳过的一点:通用循环被设计成不必直接知道终端、远程协议或会话文件。v1.0.0 还把原来放在 agent 包里的实验性 harness 整个移走了,会话和压缩落到了 coding-agent。由此可以推断,通用循环是独立于产品外壳的;但不能由依赖图推出任何一个产品都必须以同一种方式持久化会话。”

八个包分三层:谁依赖谁 ​

小A看着表格还是觉得散。老Z拿过笔,在纸上画了一张三层图:“按依赖方向分层,关系一目了然。箭头表示‘依赖’,上层调下层,下层不知道上层存在。括号里是本书不整包展开、但会点名的包。”

text
┌─────────────────────────────────────────────────────────┐
│  产品层                                                   │
│  ┌────────────────────────────┐   ┌──────────────┐       │
│  │ pi-coding-agent            │   │ pi-evals     │       │
│  │ (命令行产品、会话、工具、扩展) │◀──│ (私有评测)    │       │
│  └──────┬──────────┬──────────┘   └──────────────┘       │
└─────────┼──────────┼─────────────────────────────────────┘
          │          │
┌─────────┼──────────┼─────────────────────────────────────┐
│  核心层 ▼          ▼                                       │
│  ┌──────────────┐  ┌────────────┐  ┌────────────┐         │
│  │pi-agent-core │  │ pi-client  │  │ pi-server  │         │
│  │(Agent + 循环) │  │(远程客户端) │  │(请求路由)   │         │
│  └──────┬───────┘  └─────┬──────┘  └─────┬──────┘         │
└─────────┼────────────────┼───────────────┼────────────────┘
          │                └───────┬───────┘
┌─────────┼────────────────────────┼────────────────────────┐
│  基础层 ▼                        ▼                          │
│  ┌────────┐  ┌─────────┐  ┌────────────┐                   │
│  │ pi-ai  │  │ pi-tui  │  │pi-protocol │                   │
│  │(模型流) │  │(终端UI) │  │(协议编解码) │                   │
│  └────────┘  └─────────┘  └────────────┘                   │
│  (telemetry、chord、codemode、mcp、durable)               │
└───────────────────────────────────────────────────────────┘

“读这张图有三个要点。”老Z说:

  • 基础层三个包互不依赖:pi-ai 只管模型流,pi-tui 只管终端,pi-protocol 只管字节编解码。pi-ai 依赖 telemetry,pi-protocol 依赖 chord,这两个本书只在用到时点名。
  • 核心层搭在基础层上:pi-agent-core 只依赖 pi-ai;pi-client 和 pi-server 都依赖 pi-protocol 和 chord。v1.0.0 的 pi-server 不再依赖 AI 或 CLI,它只做握手、路由和附着。
  • 产品层把下面的零件装配起来:pi-coding-agent 直接依赖 agent、ai、tui、protocol、client、server,以及 chord、codemode、mcp。pi-evals 作为开发依赖使用 ai 和 coding-agent。产品层最厚,v1.0.0 里会话和压缩也在这一层。

小A:“那读源码该从哪层开始?”

“从核心路径开始读。pi-ai 和 pi-agent-core 是整条主路径的脊梁。基础层按需穿插(读到协议再翻 protocol,读到终端再翻 tui)。”老Z说,“这就是本书源码篇 13 章的阅读顺序:先源码篇第2–5章打通模型、循环和会话,再源码篇第6–8章看终端和产品,最后源码篇第9–11章看协议、远程和评测;源码篇第12章收束端到端主线。源码篇第13章再补一台引擎——pi-durable 怎么用原子提交和任务检查点回答‘会话凭什么活过崩溃’。”

从公开导出辨认可替换面 ​

小A:“入口在哪儿?就从 index.ts 开始看吗?”

“对,但先分清根入口和子路径。”老Z打开 packages/ai/src/index.ts:

ts
// packages/ai/src/index.ts(模块入口;v1.0.0,节选)
// Core only, side-effect free: no generated catalogs, no provider factories,
// no api-registry, no OAuth implementations, no compat. Provider factories
// live under "@earendil-works/pi-ai/providers/*", API implementations under
// "@earendil-works/pi-ai/api/*", the old global API under
// "@earendil-works/pi-ai/compat".
export * from "./api/lazy.ts";
// ……其余为类型导出与 models、types 等模块

“注意开头的注释:根入口只放核心、没有副作用;Provider 工厂在 providers/*,API 实现在 api/*,旧的全局 API 在 compat。这不是单纯的目录约定,package.json 的 exports 也列出了这条子路径边界。”老Z说,“上面的代码只说明根入口可得到哪些 API,一次请求按什么顺序走,这里看不出来。”

他又打开 packages/agent/src/index.ts:“v1.0.0 这边只剩五行:导出 agent.ts、agent-loop.ts、proxy.ts、types.ts 和 setDefaultStreamFn。会话存储、压缩和 skills 都不在这里。它说明通用 Agent 不绑定任何持久化方式,把‘Agent’等同于‘必定写 JSONL’是错误前提;写 JSONL 的是 coding-agent。”

导出层调用者能依赖的东西不能从这里推出的结论
pi-ai 根入口Models、事件流、类型、懒加载工具已注册哪些 Provider
pi-ai/providers/*某个 Provider 工厂认证已配置或模型一定可用
pi-ai/api/*某种 API 的请求/转换实现其他 API 也具备相同行为
pi-agent-core 根入口Agent、循环与回合插口宿主采用哪种存储或 UI
pi-coding-agent 二进制pi CLI 的产品入口远程模式或所有扩展已启用

一条主路径,而非完整架构图 ​

小A:“那我到底沿着哪条线读?总不能把每个包都读一遍。”

“接下来主干上的章节按控制流走,不按目录念文件名。”老Z画了一条带站号的主路径,每一站就是一章,从模型请求出发,经过循环和会话,最后拼回完整请求。源码篇第13章不在这条主干上:它另起一台引擎——pi-durable——回答源码篇第5章留下的崩溃恢复问题:

text
  ①模型请求    ②流式适配    ③循环推动    ④会话存活
  ┌────────┐   ┌────────┐   ┌────────┐   ┌──────────┐
  │ch2    │──▶│ch3    │──▶│ch4    │──▶│ch5      │
  │pi-ai   │   │pi-ai   │   │pi-agent│   │coding-   │
  │models.ts│   │lazy.ts │   │loop.ts │   │agent 会话 │
  └────────┘   └────────┘   └────────┘   └──────────┘
                                              │
  ⑤终端刷新    ⑥产品启动    ⑦工具信任         ▼
  ┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐
  │ch6    │   │ch7    │──▶│ch8    │   │ch12    │
  │pi-tui  │   │coding- │   │coding- │   │端到端  │
  │main-scr│   │agent   │   │agent   │   │完整请求│
  └────────┘   └────────┘   └────────┘   └────────┘
                   │              │           ▲
  ⑧协议字节    ⑨远程一致    ⑩评测判断      │
  ┌────────┐   ┌────────┐   ┌────────┐      │
  │ch9    │──▶│ch10    │   │ch11    │──────┘
  │protocol│   │server  │   │evals   │
  │codec.ts│   │router  │   │harness │
  └────────┘   └────────┘   └────────┘

“这张图要分组读。”老Z说:

  • 主线是 ①→②→③→④:模型请求出发,流穿过适配层,循环推动状态,会话活过进程。前三站在 pi-ai 和 pi-agent-core;v1.0.0 的会话在 coding-agent,所以第④站已经进入产品层的核心部件。先读这四章。
  • ⑤⑥⑦是产品层:终端怎么刷新、产品怎么启动、工具和信任怎么接。它们搭在核心层之上,第二组读。
  • ⑧⑨⑩是远程和评测:协议怎么变字节、远程怎么保持一致、评测怎么判断。它们是独立的能力边界,按需读。最后 ch12 把所有站拼成一次完整请求。

每章读哪个包的哪些文件 ​

小A:“那具体到文件呢?我打开 ch2,到底该翻开哪个文件?”

老Z列了一张完整对照表:“这张表是你读源码篇的导航仪。打开任何一章,先看它在哪个包、读哪些核心文件。”

章你在读哪个包核心文件(打开这几个就够)这一站回答什么
1 地图全部八包package.json × 8八包怎么分层、主路径怎么走
2 模型请求pi-aimodels.ts、auth/resolve.ts一次请求怎么从认证走到 Provider
3 流式适配pi-aiapi/lazy.ts、api/openai-responses.ts厂商事件怎么成为统一事件
4 循环pi-agent-coreagent.ts、agent-loop.ts模型输出怎么推动状态前进
5 会话pi-coding-agentcore/agent-session.ts、core/session-manager.ts会话怎么活过一次进程
6 终端pi-tuitui-main-screen.ts、stdin-buffer.ts终端为什么能稳定刷新
7 启动pi-coding-agentmain.ts、core/agent-session-services.ts命令行产品怎么启动
8 工具信任pi-coding-agenttools/file-mutation-queue.ts、extensions/runner.ts工具和扩展怎么安全接入
9 协议pi-protocolcodec.ts、framing.ts、protocol.ts消息怎么变成可靠字节
10 远程pi-server + pi-clientserver.ts、session-router.ts、client.ts远程会话怎么握手、路由和附着
11 评测pi-evalssrc/harness.ts、src/plan.ts、src/report.ts评测怎么判断一次修改
12 端到端跨全部包(回顾源码篇第2-11章的文件)一次完整请求怎么走完
13 耐久运行时pi-durablesession/session.ts、harness/scheduler.ts、harness/tool.ts、storage/一次提交与工具调用怎样在崩溃后恢复

“注意,这条线上有两条数据,别混成一条。”老Z又列了一张表:

数据在主路径中的形状关键边界
配置model、headers、认证、signal、重试选项ModelsImpl.applyAuth 合并后才交 Provider
上下文AgentMessage[],调用模型前转为 AI Message[]streamAssistantResponse 的 convertToLlm
生成AssistantMessageEventStream 的 start、delta、done/erroradapter 产生,loop 消费
副作用tool result、session append、终端刷新Agent event 与 AgentSession 写会话并非同一件事

小A:“为什么不直接从 CLI 入口开始,一路读到网络请求?那不是更直观吗?”

“那条线会同时带入配置、交互模式、终端和扩展。”老Z说,“先锁住跨包稳定的流与循环边界,才不会把某个产品装配误当成核心机制。之后的 源码篇第7章 户口本上的住址——pi-coding-agent 会再把 CLI 接回这条线。”

阅读时如何保持证据强度 ​

小A翻开笔记本,准备开始读代码。老Z拦住他:“先记住:源码走查容易犯三类错误,我踩过,别踩。”

阅读时三类常见错误

  1. 把注释或描述扩张为运行时保证:pi-server 的描述含 experimental,只能确认它自称实验性,不能自行补成“不适用于任何生产场景”。
  2. 把类型可选性误读为实现一定缺失:Provider.refreshModels? 是可选方法,代表静态 Provider 可以没有刷新路径;不表示所有动态 Provider 都会成功刷新。
  3. 把一个错误结果压扁成 false:未知 Provider、未配置认证、流中止、工具失败、JSONL 解析失败所在层不同,恢复策略也不同。后续章节会把它们放在同一张失败语义表里比较。

“推荐按这个顺序读固定快照:先读导出和类型;再读入口函数的完整分支;随后跟到下一包的接口实现;最后用测试或一个可控调用验证‘正常、错误、中止’三条路径。遇到跨层推断时,标记为推断,不替源码补作者动机。”

这套切分的收益与代价 ​

“这样切分,值吗?”小A问。

“工程观点:以包边界加调用链组织阅读,收益是替换模型适配器、会话后端或 UI 时可以先找契约,不必先理解所有产品代码。”老Z说,“代价是读者要在多处源文件之间往返,且不看具体宿主时不会知道默认配置从哪里来。”

小A接着问:“那本章故意没讲什么?”

“各 Provider 的完整模型目录、每个 API 的协议字段、工具定义、TUI 的事件订阅实现、Protocol 的 framing、Server/Client 的重连,以及评测用例。”老Z说,“这些留给对应的调用链章节,本章不展开。”

把主路径拆成可停下来的阅读站 ​

小A:“每章读多长合适?一口气跟完整条链?”

“别。源码阅读不该把一条链当作必须一次读完的长函数,每到一个公开类型或事件边界就停下来,写下输入、输出与失败结果。这比只记录‘这个包做什么’更容易发现跨层假设。”老Z给了一张“阅读站”表:

阅读站最小问题下一站失败先看哪里
模型选择Model.provider 指向谁ModelsImpl未知 Provider
请求准备credential 与 options 如何合并Provider.stream*auth、header transform
流适配上游事件如何成为统一事件runLooperror、aborted、pending
Agent turn什么进入下一次 context工具执行/队列length、tool error、terminate
会话持久化哪些 event 被追加AgentSession/SessionManager写入失败、坏行被跳过
宿主显示谁订阅事件并刷新TUI渲染、终端、输入边界

“举个例:一个网络失败,不该直接从‘请求准备’跳到‘终端显示’解释。先确认 Provider 是否将其归一为 error event;再确认 loop 是否把最终 error message 写入 context;最后才看会话层或 UI 是否记录或显示它。”老Z说,“这是一种阅读顺序,不是源码承诺的运行时重试链。”

小A:“那每章只读一个函数,会不会错过跨文件状态?”

“不会只读一个函数;关键是每章规定一个入口、一个出口和几条分支。”老Z说,“当函数跨文件调用时,继续跟其直接契约,而不是先把整个包读完。在固定提交上,git grep 可以验证符号存在,但符号存在不能代替从调用点读到返回点,类型名相同也不代表语义相同。例如 AI 层的 Context 与会话树条目处在不同抽象层,本书会在每个边界显式标出这一点。”

小结 ​

这一章解决的是源码篇第一站的问题:面对 pi-mono 十几个包,该从哪下手。答案不是按目录读,而是先固定三个坐标:版本、依赖方向、主路径。

  • 版本坐标:基线是上游标签 v1.0.0(提交 a13d35a7);本书选取的八个包都是 1.0.0(pi-evals 是私有工作区包),根 monorepo 是 0.0.3,两者不能混用。八个包是阅读范围,仓库共有 13 个包。
  • 依赖坐标:八包分三层。基础层(ai、tui、protocol)互不依赖;核心层(agent-core、client、server)搭在基础上;产品层(coding-agent,以及使用它的 evals)负责装配,v1.0.0 里会话和压缩也在这一层。通用循环被设计成不必直接知道终端、远程协议或会话文件。
  • 路径坐标:主路径把“输入到流、流到循环、循环到会话/UI”拆为可分别验证的边界,每一站就是一章。

记住

包边界不是进程边界。工作区名和依赖图能证明“可独立导入的单元”,不能证明运行时一定是独立进程。读源码时,先按这三层坐标定位自己,再沿调用链走,而不是逐个目录念文件名。

源码走查 ​

  1. 克隆上游仓库并检出标签:git clone https://github.com/earendil-works/pi && cd pi && git checkout v1.0.0,再执行 git rev-parse HEAD;预期完整输出是 a13d35a742c6ef8462812a28fbe1d8c8b7431c32。本书仓库的 pi/ 目录是同一标签的带注释快照,没有自己的 git 历史,路径相同、行号会偏移。
  2. 执行 node -p 'require("./packages/ai/package.json").version' 和同样的 agent 包命令;确认它们是 1.0.0,再对照根 package.json 的 version,避免混用版本。
  3. 打开 packages/ai/package.json 的 exports;分别记录根入口、./providers/*、./api/* 与 ./compat,不要把子路径当成根导出。
  4. 打开 packages/agent/package.json 的 dependencies;确认其中有 @earendil-works/pi-ai,再检查它没有 pi-tui,以验证上表的直接依赖说法。
  5. 逐行读 packages/ai/src/index.ts 的开头注释及导出;区分运行时 export * 和只导出类型的 export type。
  6. 读 packages/agent/src/index.ts 的五行导出;再在 packages/coding-agent/src/core/ 下找到 session-manager.ts、compaction/ 和 skills.ts,确认会话、压缩和 Skills 在 v1.0.0 属于产品层。