开源编程 Agent pi 的仓库结构导读:七个包各管什么、依赖怎么排

2026-07-29

本文基于 pi v0.82.1(仓库 commit 027a584,2026-07-28)梳理,该项目仍在快速迭代,具体行为以官方文档 https://pi.dev/docs/latest 与仓库最新代码为准。

pi 这个仓库的包边界不是按「功能模块」切的,是按「能不能被单独换掉」切的。 想明白这一条,七个工作区的位置一次就能全部落位:模型接入能换(有人要接自己的服务商)、终端渲染能换(有人要跑在浏览器或服务端)、会话存储能换(JSONL 换 SQLite)、CLI 本体是这些可换件的一个具体组装。反过来,凡是不打算让人替换的东西,它就不给你一个包。

站内已有的 Agent 框架横向对比SDK 与框架的边界 讲的是通用方法论——该怎么选、边界画在哪。本篇不重复那些结论,只做一件事:把 pi 这一个真实项目的拆法摊开,看那些原则落到具体代码库里到底长什么样。你可以边读边打开仓库对照,每个路径都能当场核。

一、先读根目录 package.json 的两行

不要从 README 开始猜结构,从根 package.json 开始。它有两处直接把答案写死了。

第一处是 workspaces

"workspaces": [
  "packages/*",
  "packages/storage/*",
  "packages/coding-agent/examples/extensions/with-deps",
  "packages/coding-agent/examples/extensions/custom-provider-anthropic",
  "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo",
  "packages/coding-agent/examples/extensions/sandbox",
  "packages/coding-agent/examples/extensions/gondolin"
]

注意后五条:几个示例扩展被直接纳入了工作区。这说明扩展不是文档里的示意代码,它们跟主包一起装依赖、一起被类型检查。示例扩展写坏了,npm run check 会红。

第二处是 build 脚本,它是串行 cd 拼出来的:

cd packages/tui && npm run build &&
cd ../ai && npm run build &&
cd ../agent && npm run build &&
cd ../storage/sqlite-node && npm run build &&
cd ../../coding-agent && npm run build &&
cd ../server && npm run build

这行命令就是依赖图的拓扑序,作者把它手写死了。你要判断谁依赖谁,读这一行比读七个 package.json 快。顺序是 tui → ai → agent → storage/sqlite-node → coding-agent → server。

二、七个包各管什么

README 的「All Packages」表只列了四个发布包,但仓库 packages/ 下实际有七个工作区。下面这张表把两边合起来,仓库位置全部是可以当场打开的真实路径。

组成部分它负责什么仓库位置你什么时候会碰到它
@earendil-works/pi-ai统一多家模型服务商的 LLM 接口,模型目录与 provider 实现packages/ai接入新服务商、排查请求转换、改模型元数据
@earendil-works/pi-agent-coreagent 运行时:工具调用、状态管理、会话持久化与编排packages/agent把 agent 能力嵌进自己的产品
@earendil-works/pi-tui终端 UI 库,差分渲染、编辑器组件、键位packages/tui改交互界面、加快捷键、修渲染错位
@earendil-works/pi-coding-agent交互式编程 agent CLI,pi 命令本体packages/coding-agent加内置工具、写扩展、调 CLI 行为
@earendil-works/pi-storage-sqlite-nodenode:sqlite 适配与 SQLite 会话仓库、迁移、物化视图packages/storage/sqlite-node会话从 JSONL 换成 SQLite
@earendil-works/pi-server标注为 experimental 的 server 包packages/server想把 pi 放到进程外被调用
@earendil-works/pi-evalsprivate: true 的评测工作区,npm run eval 入口packages/evals做回归评测

有两个细节值得单独说。

一是根 package.jsonversion 和子包的版本号根本不是一回事,两者数值差得很远。根包带 private: true,既不发布也不跟着子包走,它那个版本号别拿来当项目版本用。真正的版本策略写在 AGENTS.md 里:所有包 lockstep 同版本发布,patch 用于修复和新增,minor 用于破坏性变更,不发 major。

二是 packages/coding-agent/docs/development.md 里的「Project Structure」还是四个包的老视图(ai / agent / tui / coding-agent)。文档比目录滞后,以 workspaces 和构建脚本为准。这种滞后在快速迭代的项目里很常见,读源码时把文档当线索、把配置当事实。

三、依赖方向:单向、无环、两个叶子

把七个 package.jsondependencies 里带 @earendil-works/ 前缀的项摘出来,图就很干净:

  • pi-ai:内部依赖为空。外部依赖是各家服务商 SDK(@anthropic-ai/sdkopenai@google/genai@mistralai/mistralai@aws-sdk/client-bedrock-runtime)加上 typeboxpartial-json、代理相关的 http-proxy-agent / https-proxy-agent,以及 @opentelemetry/api
  • pi-tui:内部依赖同样为空,只有 get-east-asian-widthmarked
  • pi-agent-core:依赖 pi-ai
  • pi-storage-sqlite-node:依赖 pi-aipi-agent-core
  • pi-coding-agent:依赖 pi-agent-corepi-aipi-tui
  • pi-server:只依赖 pi-coding-agent

两个叶子节点是 aitui,它们互不知道对方存在。这是整张图里最值得学的一笔:终端渲染层完全不依赖模型层pi-tui 里没有一行代码知道什么是 token、什么是 provider,它只是一个差分渲染的终端 UI 库;反过来 pi-ai 也不知道结果会被渲染到哪里。想把 pi 的 agent 内核搬到 Web 或服务端,你砍掉的是 tui 那一条边,aiagent 原样可用。

存储为什么要单独开一个 packages/storage/* 目录层级,packages/agent/README.md 里给了原话:SQLite 会话后端和 node:sqlite 适配器放在独立包 @earendil-works/pi-storage-sqlite-node,这样核心包默认不会拉进运行时内置模块或原生 SQLite 依赖;该后端接受一个运行时相关的 SQLite factory,未来其它存储后端也能各自成包。

一句话:核心包保持「跑在哪都行」,把绑定运行时的部分推到边缘。注意 pi-coding-agent 的依赖列表里并没有 sqlite 包——CLI 默认不背这个包袱,需要的人自己装。

四、packages/agent 内部还有三层,别当成一层读

七个包只是外层。真正容易读迷路的是 packages/agent/src,它自己又是三层,从低到高:

  • agent-loop.ts:低层循环。packages/agent/README.md 把它描述为「observational」的流——保持事件顺序,但不会等你的异步事件处理结算完再推进下一阶段。它对应 agentLoop() / agentLoopContinue()
  • agent.tsAgent 类。有状态,管队列(steering / follow-up)、continuation、abort 和结算。README 里明确说,如果你需要「消息处理必须先于工具预检完成」这种屏障语义,就用 Agent 而不是裸 agentLoop()
  • harness/agent-harness.tsAgentHarnesspackages/agent/docs/agent-harness.md 的定义是「低层 agent 循环之上的编排层」,拥有会话持久化、运行时配置、资源解析、操作锁,以及面向扩展的变更语义。

这三层的分工,直接决定了你该在哪一层加东西。想拦一次工具调用,AgentbeforeToolCall / afterToolCall 就够;想让一个被中断的运行在新进程里接着跑,那是 harness 的事。加错层的典型症状是:你在低层循环里写了个需要「等它处理完再往下走」的回调,结果发现循环根本不等你。

packages/agent/docs/harness.md 是这一层的设计文档,标题是「Durable AgentHarness plan」。它把词汇表定得很死,读之前先记住五个词:

  • Harness:针对一个 session 执行运行,驱动模型请求和工具,同一时刻只有一个 harness 写一个 session。
  • Session:持久状态,一条只追加的条目日志。同一份日志有两个视图——树(会话状态)和编排历史。
  • Session entry:树里的条目,靠 parentId 定义会话祖先关系,会进模型上下文。
  • Harness entry:私有的编排事实,用来在异常终止后恢复运行。同在一条日志里,但没有 parent、不进模型上下文、不对外发出。
  • Ref:指向树的某个叶子的可移动命名指针,加上串行在它上面的工作。每个 session 默认有一个 main

文档里那条中心不变量写得很直白:session entry 定义会话是什么,harness entry 定义 harness 做了什么、按什么顺序做的;日志顺序决定编排历史,parentId 和每个 ref 的叶子指针决定分支,harness entry 永远不改变树的拓扑。

配套的还有一条「append-only context」不变量:同一分支的多次请求之间,模型上下文只在尾部增长;在上一次请求的尾部之前插内容,会让服务商的 KV 缓存从插入点开始失效,token 成本被静默放大。这条不变量正是「运行中的写入要延迟到 checkpoint」的真实理由——不只是为了工具调用与结果相邻。压缩是唯一被承认的例外,它拿一次完整缓存失效换更小的上下文。这个取舍和 上下文管理 讲的是同一件事,只是这里能看到它被写成硬性的代码约束。

存储后端在这份文档里也定了三种:JSONL、内存、SQLite。后端只实现追加、读取和查找查询,对 operation、队列、恢复一无所知。这就闭合到了上一节的分包理由。

五、拿到仓库,从哪个文件开始读

harness.md 的第 20 节 Required reading 直接给了一份阅读清单,作者写它本来是给新实现会话用的,但对新读者同样好用。它的顺序是:先四份设计文档(harness.mdagent-harness.mdhooks.mdobservability.md,都在 packages/agent/docs/),再读当前实现——packages/agent/src/agent-loop.tsagent.tsharness/agent-harness.tsharness/types.tsharness/session/session.tsharness/session/jsonl-storage.ts,然后是 packages/coding-agent/src/core/agent-session.tspackages/coding-agent/src/core/extensions/runner.ts,最后是 SQLite 后端和一批行为测试。

清单里有一条路径对不上:它写的是 packages/ai/src/utils/transform-messages.ts,实际文件在 packages/ai/src/api/transform-messages.ts。这类小偏差在活跃仓库里正常,遇到就顺手核一下,别怀疑自己。

如果你不打算通读,按目的挑起点更快:

  • 只想改 CLI 行为packages/coding-agent/src/cli.ts 只有 20 行,是 bin 字段 pi 指向的入口;真正的重头在 packages/coding-agent/src/core/agent-session.ts,3000 行以上,队列、bash、扩展、重试、压缩流程都在那里。
  • 想把 agent 嵌进自己的产品:先读 packages/agent/README.md,它把事件序列、AgentMessage 与 LLM message 的关系、convertToLlm / transformContext 的分工都画成了流程图,比直接看类型定义快得多。
  • 想加存储后端harness.md 第 14 节写了后端契约(一个跨 session entry 和 harness entry 的全序 seq、append 的 promise resolve 即持久、entry id 在 session 内唯一、读出的是不可变快照、一个 session 一个写者),再对照 packages/storage/sqlite-node/src/sqlite/repo.ts
  • 想改模型接入packages/ai/src/providers/ 下每家一个文件,packages/ai/src/api/ 下是各家协议的请求实现。

六、边界与代价:它明确不管什么

看清一个项目放弃了什么,比看清它做了什么更有用。

它不做权限系统。 README 里写得毫不含糊:pi 不包含用于限制文件系统、进程、网络或凭据访问的内置权限系统,默认以启动它的用户和进程的权限运行。要更强的边界,就自己容器化或沙箱化,packages/coding-agent/docs/containerization.md 给了三种模式——Gondolin 扩展(把 pi 和 provider 认证留在宿主机,把内置工具和 ! 命令路由进本地 Linux 微虚拟机)、纯 Docker(整个进程丢进容器)、OpenShell(整个进程跑在策略受控的沙箱里)。如果你的团队对权限最小化有硬要求,这部分工作量要算在自己头上,参考 最小权限设计 自己补。

它不保后向兼容。 AGENTS.md 里的规则是:除非用户明确要求,不保留后向兼容。配上 lockstep 版本策略(minor 才是破坏性变更、不发 major),意味着一次 minor 号变动就可能把 API 换掉,而 patch 号里既有修复也有新增。把它作为库依赖进生产,锁版本、留升级预算。

它不管 Slack 和聊天类自动化。 README 直接把这块指向另一个仓库 earendil-works/pi-chat。主仓库不背这些集成。

JSONL 的持久性只到进程崩溃级。 harness.md 第 14 节说得很清楚:一次 resolve 的 appendFile 调用即算持久,不承诺 fsync、不承诺掉电安全;如果哪天需要,那会是一个显式能力,而不是隐含保证。文件最后一行如果写坏了,会被当作崩溃残留截断——那条 append 从未被确认过,所以不算丢;但中间任何一行坏了就是损坏,直接拒绝打开。

TypeScript 语法被砍了一刀。tsconfig.base.json 打开了 erasableSyntaxOnly,根 tsconfig.jsoninclude 把它罩在 packages/*/srcpackages/*/testpackages/storage/*/srcpackages/storage/*/testpackages/coding-agent/examples 上,也就是全部源码加测试加示例扩展。这个开关只允许可擦除语法:不能用参数属性、enumnamespace / moduleimport =export = 这些需要 JS emit 的构造。这是为了让源码能被直接剥离类型运行,代价是你熟悉的一些写法直接不可用。

它对上下文压缩和重试的处理是有主张的,不是可插拔的。 压缩、重试策略、队列语义都在 harness 层写死了契约,扩展点是 hook 而不是替换实现。重试分两层:streamOptions.maxRetries 管一次请求内部的传输重试,retry 策略管跨失败请求的 harness 级重试,且尝试次数是持久的、重启不清零。这个设计取向可以对照 失败重试策略 判断合不合你的场景。

最后一点跟国内工程师直接相关:packages/ai 打包的那几家服务商 SDK 里,多家海外厂商对中国大陆的服务可用性和账号注册都有各自的区域政策,直连未必可行。仓库本身只负责把请求发出去,能不能通、合不合规不在它的职责范围内。市面上确实存在第三方中转,但稳定性、合规与数据流向都要你自己判断,本文不做任何背书。各家规则不同且会调整,以官方最新说明为准。

七、上手与避坑清单

每条都写清楚为什么会踩,以及怎么避。

1. 直接 npm install 会跑生命周期脚本。 这个仓库把依赖安装当成安全面对待:README 和 AGENTS.md 都要求用 npm install --ignore-scripts(CI 用 npm ci --ignore-scripts)。会踩是因为大多数项目 npm install 就完事了,肌肉记忆会让你漏掉这个 flag。避法:把 --ignore-scripts 写进你自己的 setup 笔记,别依赖记忆。

2. 直接跑全量 vitest 会打真实服务商。 AGENTS.md 明确禁止直接跑完整 vitest 套件,因为里面包含 e2e 测试,只要环境里存在 endpoint 或 auth 相关环境变量它们就会激活——那是真花钱的请求。会踩是因为 npx vitest 是本能反应。避法:非 e2e 一律用仓库根的 ./test.sh;要跑单个文件就从包目录执行 node ../../node_modules/vitest/dist/cli.js --run test/xxx.test.ts。写 coding-agent 的测试则用 packages/coding-agent/test/suite/harness.ts 加 faux provider(packages/ai/src/providers/faux.ts),不碰真实 API。

3. 改 packages/ai/src/models.generated.ts 是白改。 名字里的 generated 已经提示了,但它就摆在 src 下,很容易被搜索命中后直接编辑。AGENTS.md 的规则是:改 packages/ai/scripts/generate-models.ts,然后重新生成。避法:看到 .generated. 先找生成脚本。

4. 用了 enumnamespacenpm run check 会拦。 上一节说的可擦除语法限制。会踩是因为这在别的 TypeScript 项目里完全合法。避法:需要枚举就用 union 字面量类型加 const 对象;类里别用参数属性,显式写字段再在构造函数里赋值。

5. git add -A 会踩到别人的活。 AGENTS.md 有一整节讲这个:同一个工作目录里可能同时跑着多个 pi 会话,各改各的文件。所以规则是只提交自己这一轮改的文件、显式列路径、提交前先 git status 核对,并且明令禁止 git reset --hardgit checkout .git clean -fdgit stashgit add -Agit add .git commit --no-verify。会踩是因为这些命令平时是安全的。避法:在这个仓库里把它们当成破坏性操作对待。

6. 顺手提交了 lockfile 会被 pre-commit 拦下。 package-lock.json 是依赖事实的唯一来源,pre-commit 默认阻止意外提交,除非设了 PI_ALLOW_LOCKFILE_CHANGE=1。会踩是因为跑一次 install 就可能改动它。避法:确实要改依赖时才显式带上这个环境变量,其余情况把 lockfile 的改动撤掉。

7. 加依赖不能写范围版本。 .npmrc 里设了 save-exact=truemin-release-age——直接外部依赖锁精确版本,同时给依赖解析加了一道「发布时间不够久的新版本不参与」的年龄门槛(具体单位以 npm 文档为准)。内部工作区包不受这条约束,仍然保持范围版本,check-pinned-deps.mjs 里对 @earendil-works/pi- 前缀的包专门做了放行。npm run check 里的 check:pinned-deps 会验证这一点。会踩是因为 ^ 是 npm 默认行为。避法:加依赖后跑一次 npm run check 再提交。

8. 装第三方 pi 包等于执行任意代码。 packages/coding-agent/docs/packages.md 顶上就是安全提示:pi 包以完整系统权限运行,扩展执行任意代码,skill 可以指示模型做任何事,包括运行可执行文件。会踩是因为 pi install npm:xxx 看起来跟装个 npm 库一样轻。避法:装之前读源码;只想试用就用 -e 装到临时目录、仅对本次运行生效。如果你在团队里推广,这条得单独写进规范,而不是靠口头提醒。

八、收个尾

如果只带走一件事:在这个仓库里,「哪一层能被替换」比「哪一层功能多」更能解释目录结构aitui 是两个互不相识的叶子,agent 把它们中的模型那一半包成运行时,coding-agent 是这些可换件的一次具体组装,storage/sqlite-nodeserver 是被推到边缘的可选件。

给你一份最短的自检清单,读完能答上来就说明结构在你脑子里立住了:

  1. 说出 aitui 各自的内部依赖分别是什么。(都是空)
  2. 说出 SQLite 后端为什么不在 packages/agent 里。(核心包不背运行时内置模块和原生依赖)
  3. 说出 agentLoop()AgentAgentHarness 三者各自负责什么。
  4. 说出 session entry 和 harness entry 的区别。(一个定义会话是什么,一个定义 harness 做了什么)
  5. 说出这个项目明确不做的三件事。(内置权限系统、后向兼容、聊天类集成)

接下来该打开哪个文件,取决于你要干什么:改 CLI 就去 packages/coding-agent/src/core/agent-session.ts;嵌入 agent 就先读 packages/agent/README.md;想理解持久化与恢复的完整设计,packages/agent/docs/harness.md 从第 2 节术语表开始读,那份术语表定得越死,后面读代码越省力。项目采用 MIT 许可证,主仓库在 https://github.com/earendil-works/pi ,截至 2026 年 7 月 GitHub 上约 8 万 star。仓库随时在变,所有路径以你手上那份 checkout 为准。

本篇属于一个把开源编程 Agent 项目 pi 逐层拆开讲的系列,整体地图见开源编程 Agent pi 是什么;沿着这条线往下,还可以看 开源编程 Agent pi 的评测包开源编程 Agent pi 的统一模型接口

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。