Skip to content

第18章 地图的比例尺——pi-mono ​

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

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

老z走过来,看了一眼屏幕,没有指任何目录,反而问:“概念篇留下的问题是什么?”

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

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

本篇的已确认事实全部以 pi-mono 的提交 583f153d502aa8e958eefdb9af0fbd3344e68f95 为准;提交日期为 2026-08-01。本书选取的下列九个核心/产品范围包的 package.json 版本均为 0.83.0,而根 package.json 的 monorepo 版本是 0.0.3,不能混用。

这个“九个”是本书的阅读范围,不是根 workspace 的总数:根 workspace 还包含扩展示例;packages/evals 是私有工作区包,不是发布包;packages/coding-agent/install-lock 是私有的安装锁根,也不在这九包范围内。

先固定地图的比例尺 ​

小a:“九个包……是不是九个独立程序?”

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

比喻:地图的比例尺

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

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

包npm 包名可确认的边界证据本书沿它追什么暂不在本章断言
packages/ai@earendil-works/pi-ai有根、api/*、providers/* 等导出模型、认证、流适配各厂商服务质量
packages/agent@earendil-works/pi-agent-core依赖 pi-ai,根和 ./node 导出循环、Harness、会话具体 CLI 策略
packages/tui@earendil-works/pi-tui独立终端 UI 包组件与渲染Agent 决策
packages/coding-agent@earendil-works/pi-coding-agentpi 二进制入口,依赖前述核心包命令行装配所有扩展行为
packages/protocol@earendil-works/pi-protocol传输中立的 CBOR 协议包帧、schema、版本网络可靠性
packages/client@earendil-works/pi-client依赖 pi-protocol远程会话客户端服务端策略
packages/server@earendil-works/pi-server实验性服务包,依赖 AI、CLI、协议服务端会话生产部署保证
packages/storage/sqlite-node@earendil-works/pi-storage-sqlite-nodeNode SQLite 会话后端持久化替代后端浏览器可用性
packages/evals@earendil-works/pi-evals私有评测工作区包用例与 judge产品质量结论

小a盯着 coding-agent 那行,忽然发现一件事:“它依赖 AI、Agent、TUI、Protocol 和 Client?那 agent 呢?”

“agent 只直接依赖 AI。”老z说,“这张表已经暴露了一个容易跳过的事实:通用循环被设计成不必直接知道终端或远程协议。由此可以推断,通用循环是独立于产品外壳的;但不能由依赖图推出任何一个产品都必须使用 Harness。”

九包分三层:谁依赖谁 ​

小a看着表格还是觉得散。老z拿过笔,在纸上画了一张三层图:“按依赖方向分层,九包的关系一目了然。箭头表示"依赖",上层调下层,下层不知道上层存在。”

text
┌─────────────────────────────────────────────────────────┐
│  产品层                                                   │
│  ┌──────────────────┐    ┌──────────────┐                │
│  │ pi-coding-agent  │    │  pi-server   │                │
│  │ (命令行产品入口)  │    │ (实验性服务端) │                │
│  └────────┬─────────┘    └──────┬───────┘                │
└───────────┼─────────────────────┼────────────────────────┘
            │                     │
┌───────────┼─────────────────────┼────────────────────────┐
│  核心层   │                     │                          │
│  ┌────────▼─────────┐  ┌───────▼────────┐                 │
│  │ pi-agent-core    │  │  pi-client     │                 │
│  │ (循环+会话+Harness)│  │ (远程会话客户端) │                 │
│  └────────┬─────────┘  └───────┬────────┘                 │
│           │                    │                          │
│  ┌────────▼────────────────────▼────────┐                 │
│  │     pi-storage-sqlite-node           │                 │
│  │     (Node SQLite 会话后端)            │                 │
│  └──────────────────────────────────────┘                 │
└───────────┼─────────────────────────────────────────────┘
            │
┌───────────┼─────────────────────────────────────────────┐
│  基础层   ▼                                               │
│  ┌────────┐ ┌─────────┐ ┌──────────┐ ┌────────┐          │
│  │ pi-ai  │ │ pi-tui  │ │pi-protocol│ │pi-evals│          │
│  │(模型流) │ │(终端UI) │ │(协议编解码)│ │(评测)  │          │
│  └────────┘ └─────────┘ └──────────┘ └────────┘          │
└─────────────────────────────────────────────────────────┘

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

  • 基础层四个包互不依赖——pi-ai 只管模型流,pi-tui 只管终端,pi-protocol 只管字节编解码,pi-evals 只管评测。它们各自独立,谁也不知道谁。
  • 核心层搭在基础层上——pi-agent-core 依赖 pi-ai(它要调模型),pi-client 依赖 pi-protocol(它要走远程),pi-storage-sqlite-node 同时依赖 ai 和 agent-core(它要存会话)。
  • 产品层把核心层和基础层装配起来——pi-coding-agent 依赖五个包,是最终的产品入口;pi-server 依赖三个包,提供远程服务。产品层最厚,但它只是装配,核心逻辑在下面两层。

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

“从核心层开始读——pi-ai 和 pi-agent-core 是整条主路径的脊梁。基础层按需穿插(读到协议再翻 protocol,读到终端再翻 tui)。产品层最后看,因为它只是把下面的零件拼起来。”老z说,“这就是本书源码篇 12 章的阅读顺序:先 17-20 章打通核心层,再 21-23 章看产品和终端,最后 24-26 章看协议、远程和评测。”

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

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

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

ts
// packages/ai/src/index.ts(模块入口;583f153d,节选)
// Provider factories live under "@earendil-works/pi-ai/providers/*",
// API implementations under "@earendil-works/pi-ai/api/*".
export * from "./api/lazy.ts";
export * from "./models.ts";
export * from "./types.ts";

“注意开头的注释——Provider 工厂在 providers/*,API 实现在 api/*。这不是目录约定,而是 package.json 的 exports 也列出的子路径边界。”老z说,“上面的代码只说明根入口可得到哪些 API,不能说明一次请求的顺序。”

他又打开 packages/agent/src/index.ts:“这边把 agent.ts、agent-loop.ts、harness/agent-harness.ts、JSONL store、repository、compaction 和 skills 都导出了。它说明宿主可以组合这些部件,也说明把‘Agent’等同于‘必定写 JSONL’是错误前提。”

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

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

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

“接下来的十二章按控制流走,不按目录念文件名。”老z画了一条带站号的主路径——每一站就是一章,从模型请求出发,经过循环和会话,最后拼回完整请求:

text
  ①模型请求    ②流式适配    ③循环推动    ④会话存活
  ┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐
  │ch17    │──▶│ch18    │──▶│ch19    │──▶│ch20    │
  │pi-ai   │   │pi-ai   │   │pi-agent│   │pi-agent│
  │models.ts│   │lazy.ts │   │loop.ts │   │harness │
  └────────┘   └────────┘   └────────┘   └────────┘
                                              │
  ⑤终端刷新    ⑥产品启动    ⑦工具信任         ▼
  ┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐
  │ch21    │   │ch22    │──▶│ch23    │   │ch27    │
  │pi-tui  │   │coding- │   │coding- │   │端到端  │
  │main-scr│   │agent   │   │agent   │   │完整请求│
  └────────┘   └────────┘   └────────┘   └────────┘
                   │              │           ▲
  ⑧协议字节    ⑨远程一致    ⑩评测判断      │
  ┌────────┐   ┌────────┐   ┌────────┐      │
  │ch24    │──▶│ch25    │   │ch26    │──────┘
  │protocol│   │server  │   │evals   │
  │codec.ts│   │sessions│   │harness │
  └────────┘   └────────┘   └────────┘

“这张图告诉你三件事。”老z说:

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

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

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

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

章你在读哪个包核心文件(打开这几个就够)这一站回答什么
16 地图全部九包package.json × 9九包怎么分层、主路径怎么走
17 模型请求pi-aimodels.ts、auth/resolve.ts一次请求怎么从认证走到 Provider
18 流式适配pi-aiapi/lazy.ts、api/transform-messages.ts厂商事件怎么变成统一事件
19 循环pi-agent-coreagent.ts、agent-loop.ts模型输出怎么推动状态前进
20 会话pi-agent-coreharness/agent-harness.ts、session/jsonl-store.ts会话怎么活过一次进程
21 终端pi-tuitui-main-screen.ts、stdin-buffer.ts终端为什么能稳定刷新
22 启动pi-coding-agentmain.ts、core/agent-session-services.ts命令行产品怎么启动
23 工具信任pi-coding-agenttools/file-mutation-queue.ts、extensions/runner.ts工具和扩展怎么安全接入
24 协议pi-protocolcodec.ts、framing.ts、schemas.ts消息怎么变成可靠字节
25 远程pi-server + pi-clientsessions.ts、snapshots.ts远程会话怎么保持一致
26 评测pi-evalspi-harness.ts、vitest-evals/setup.ts评测怎么判断一次修改
27 端到端跨全部包(回顾 17-26 的文件)一次完整请求怎么走完

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

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

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

“那条线会同时带入配置、交互模式、终端和扩展。”老z说,“**先锁住跨包稳定的流与循环边界,才不会把某个产品装配误当成核心机制。**之后的 第24章 户口本上的住址——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 被追加repository/storesession、解析、写入失败
宿主显示谁订阅事件并刷新TUI渲染、终端、输入边界

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

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

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

小结 ​

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

  • 版本坐标:本书选取的九个包都是 0.83.0(pi-evals 是私有工作区包),根 monorepo 是 0.0.3,两者不能混用。九个包是阅读范围,不是仓库全貌。
  • 依赖坐标:九包分三层——基础层(ai、tui、protocol、evals)互不依赖;核心层(agent-core、client、sqlite-node)搭在基础上;产品层(coding-agent、server)只是装配。通用循环被设计成不必直接知道终端或远程协议。
  • 路径坐标:主路径把"输入到流、流到循环、循环到会话/UI"拆为可分别验证的边界,每一站就是一章。

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

源码走查 ​

  1. 在 pi-mono 根目录执行 git rev-parse HEAD;预期完整输出是 583f153d502aa8e958eefdb9af0fbd3344e68f95。
  2. 执行 node -p 'require("./packages/ai/package.json").version' 和同样的 agent 包命令;确认它们是 0.83.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 搜索 createJsonlSessionStore、compact、loadSkills;确认它们是公开可组合 API,而不是从导出关系推断它们必然同时运行。