每日壁纸

一句话项目 slogan:把 agent harness 的默认值还给你。

这是「一天一个开源项目」系列的一篇。今天讲 Pi。

现在用 coding agent 的人,大概都碰过同一类麻烦:工具替你做了太多决定。权限要不要每次弹窗、上下文满了怎么压缩、要不要 plan mode、要不要 sub-agent——这些默认值都焊在产品里,不喜欢的部分只能等发版。等你真想把规矩改成自己的(拦掉某类命令、接一个内部模型、把会话导进自家系统),才发现挂载点不够用,或者挂载点本身就是留给厂商自家功能的。

Pi 走的是反方向。它把自己定义成 "a minimal, extensible agent harness that you can make your own",README 里写得直白:有意跳过 sub-agent 和 plan mode,缺什么自己写扩展,或者从 npm / git 装一个别人写好的包。

项目速览

⭐ 116,957 Stars | 🍴 14,940 Forks | 📄 MIT 协议 | 🔧 7,043 次提交 | 🐛 366 个 Open Issues

截至 2026-10-22。npm 包 @earendil-works/pi-coding-agent 上周下载 271 万次(2026-10-12 至 10-18),v1.4.0 发布于 2026-10-21。作者是 libGDX 的 Mario Zechner(badlogic),贡献榜第二位是 Flask 作者 Armin Ronacher(mitsuhiko),911 次提交。

阅读价值与前置说明

读完你应该能回答:

  • 为什么 Pi 把会话存成一棵树,而不是一个消息数组?压缩为什么是「追加一条记录」而不是「覆盖历史」?
  • 一个明确说自己没有权限系统的 agent,靠什么划安全边界?project trust 信任了什么,又没信任什么?
  • 11 个包的依赖图为什么不是一条链,pi-durable 为什么不在 CLI 的依赖里?
  • TypeScript 扩展为什么能直接写、直接加载,不需要先编译?
  • 压缩的切点为什么绝对不能切在 tool result 上?

前置知识:

  • 用过任意一个 coding agent(Claude Code / Codex / Aider 都行)
  • 知道 JSONL 和 append-only 日志大概是怎么回事
  • 可选:了解 WASM 沙箱、终端 ANSI / TUI 渲染,读「深度设计剖析」会更顺

项目背景与完整档案

官方定位(README 第一句):"Pi is a minimal, extensible agent harness that you can make your own." 它不是一个 IDE 插件,是独立的 CLI 加 TypeScript SDK,把自己的一堆能力拆成可单独安装的库。

社区属性上有个容易搞混的点:Pi 最早挂在 Mario Zechner 的个人仓库 badlogic/pi-mono,npm 包用 @mariozechner/* 命名空间。按 pi.dev 官方公告,项目后来搬到 earendil-works/pi,包名统一改成 @earendil-works/*,0.74.0 是新命名空间下的第一个版本。旧包没有下架,只标了 deprecated——这样做是为了让旧安装和 pin 过的依赖仍然可复现。

基础信息:

  • npm 包名:@earendil-works/pi-coding-agent,当前版本 1.4.0(2026-10-21 发布)
  • 协议:MIT
  • 运行环境:Node.js 22.19 及以上;macOS / Linux / Windows,另支持 Nix
  • 仓库结构:11 个 workspace 包,版本号由脚本统一同步,当前全部 1.4.0
  • 主要语言:TypeScript 14.5 MB,JavaScript 402 KB,C 38 KB
  • 模型支持:packages/ai 里 40 多个 provider 适配

项目数据(截至 2026-10-22):

  • ⭐ Stars 116,957 | 🍴 Forks 14,940
  • 🔧 提交 7,043 | 🐛 Open Issues 366
  • 👥 贡献者 292,前两名是 badlogic(3,941)和 mitsuhiko(911)
  • 🗓️ 创建于 2025-08-09,最近推送 2026-10-21
  • 📦 GitHub Releases 275 个;coding-agent 的 CHANGELOG 里有 291 个版本段

分层这事值得单独说一句,因为它不是一条链。pi-telemetry 在最底下(pi-ai 和 chord 都依赖它),pi-ai 之上才有 pi-agent-core;pi-tui、pi-codemode、pi-mcp 是叶子,不依赖任何内部包;pi-durable 走的是另一条支线(chord + pi-ai + pi-codemode),服务会话/任务/文档持久化和 pi-env,CLI 本身并不依赖它。README 那张表看着像五层蛋糕,实际是一个浅 DAG——每一层薄到能单独拿走用,这是「可嵌入」的前提。

它解决什么问题

coding agent 现在的普遍痛点:

  • 默认值不由你定:权限弹窗、plan mode、sub-agent、压缩策略,全是产品决策。你不同意,只能等下一个版本
  • 压缩是黑盒:上下文快满,工具压一刀。压完你不知道它记住了什么、丢了什么,也没法「回到压缩之前那个点」继续
  • 扩展被锁在生命周期里:想加一个工具、一个命令、一个内部模型,得看宿主给不给挂载点,以及挂载点会不会在下个版本改签名
  • 数据不在你手里:会话存在哪、什么格式、能不能导出、能不能 diff,取决于厂商

Pi 的解法是三步递进的:

第一步,把 harness 拆成包。pi-ai 只管 provider,pi-agent-core 只管 agent loop,pi-tui 只管渲染,pi-coding-agent 才是那个 CLI。想嵌入自己的服务,直接用 SDK,不用带一个终端。

第二步,把「默认值」变成文件。扩展、skill、prompt template、theme 都是仓库里的普通文件,pi install npm:xxx 装进来就能用,不 patch 核心。

第三步,把会话变成你磁盘上的树。JSONL 追加写,每条记录有 id 和 parentId,压缩只是在树上加一个节点。

使用场景:

  • 把 agent 嵌进产品:README 里点名的 OpenClaw 就是拿 Pi 的 SDK 做的集成
  • 团队统一 agent 行为:把内网的命令规范、发布流程、代码风格检查写成扩展加 skill,打成 npm 包 pin 版本,而不是靠每个人自己写一份 CLAUDE.md
  • 长任务 / 多分支探索:一个任务干了三天,中途想回到「昨天下午那个思路」试另一条路,/tree 直接切,不用重讲背景
  • 要在隔离环境里跑 YOLO agent:Pi 不做权限系统,那就丢进容器,边界由 Docker 或 micro-VM 给

快速上手

前提:Node.js 22.19 以上(安装脚本会帮你装)。

装 CLI,官方推荐用安装脚本(会 pin 住所有依赖,后续用 pi update 更新):

curl -fsSL https://pi.dev/install.sh | sh

Windows 用 PowerShell:

powershell -c "irm https://pi.dev/install.ps1 | iex"

也可以用 npm 装,差别是 npm 不 pin 传递依赖(官方 README 明确说了这点):

npm install -g --ignore-scripts @earendil-works/pi-coding-agent

进项目目录直接启动,然后 /login 接一个订阅或 API key:

cd /path/to/project
pi

不想进交互界面,用 print 模式或 RPC 模式:

pi --print "解释一下这个仓库的构建流程"      # 一次性输出后退出
git diff | pi --print "review 这个改动"     # 从 stdin 读
pi --mode json "看看这个仓库" > events.jsonl # JSONL 事件流
pi --mode rpc                               # stdin/stdout 上的 JSON-RPC

用 Nix 的话一行搞定:

nix run github:earendil-works/pi/stable

进去之后常用的几个动作:/login 接模型、/compact 手动压缩、/tree 在会话树上切分支、/trust 记住对某个目录的信任决定。想装别人写好的扩展或技能包:

pi install npm:@example/pi-tools@1.0.0
pi install git:github.com/example/pi-tools@v1

核心特性拆解

四种运行形态——交互 TUI、print、JSON、JSON-RPC,再加一个进程内 SDK。四者共用同一套 agent 和 session 机制,只是接口不同。

8 个内置工具——read / write / edit / bash / grep / find / ls / powershell。写操作走一个 file-mutation-queue 串行化,避免并行 tool call 互相覆盖同一个文件。

扩展系统——一个导出 default factory 的 TypeScript 文件,工厂拿到 ExtensionAPI,可以注册工具、斜杠命令、快捷键、provider、事件处理器、终端 UI。用 jiti 在运行时加载,写完直接 pi --extension ./hello.ts 就能跑,不用先编译。事件挂得也细:一次 run 里有 agent_start / turn_start / context / context_with_system / agent_end,工具执行那一圈有 tool_call / tool_result / tool_execution_end,连 project_trust 和压缩前的 session_before_compact 都能接管。仓库里带了 70 个可以直接抄的示例扩展。

Pi packages——extensions / skills / prompt templates / themes 打包成一个 npm 包或 git 仓库分发,pi install npm:xxx@1.0.0 或 pi install git:github.com/xxx@v1。版本是 pin 住的,升级要显式做。

会话树——/tree 切分支、/fork、/clone、/resume;压缩既能自动触发(超过阈值),也能手动 /compact [instructions]。

codemode——让模型写 JavaScript 脚本,脚本里直接调 pi 的其它工具(tools.read(...)、tools.bash(...)),并行跑、自己过滤,只有脚本输出回给模型。

TUI 单独可用——@earendil-works/pi-tui 是可以独立 install 的终端 UI 框架,带差分渲染、备用屏、鼠标、内联图片,不是绑死在 CLI 里的私有实现。

深度设计剖析

设计亮点一:压缩不删历史,只是往树里追加一个节点

常规方案:

大多数 agent 的上下文管理就是「消息数组加截断」。超了就把最老的消息扔掉,或者让模型总结一段,然后用摘要替换原文。想法直接,代价藏在后面:一旦替换就回不去了。你想回到压缩前的某一步、从那里分叉试另一种做法,得重新描述一遍背景。

Pi 的做法:

会话文件是 JSONL,每行一个 entry,entry 之间有 id 和 parentId,构成一棵树。当前 entry 决定 active branch,模型请求只从这条链上取消息。

压缩(compaction)不是就地改写,而是往树里追加一类新节点:

// packages/coding-agent/src/core/session-manager.ts
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
  type: 'compaction'
  summary: string
  firstKeptEntryId: string
  tokensBefore: number
  details?: T
  usage?: Usage
  fromHook?: boolean
  /** Complete prompt and tool state at this compaction boundary. */
  systemMessage?: SystemMessage
}

关键是 firstKeptEntryId:它记下「摘要之后从哪条 entry 开始保留原文」。原始 entries 一条都不删,之后的模型请求用「摘要 + firstKeptEntryId 之后的消息」重建上下文。压缩因此在数据层就是一条普通记录——可撤销、可审计、可分支。

几个有信息量的细节:

  • 切点不能落在 tool result 上。 findCutPoint() 的注释写得很清楚:可以切在 user 或 assistant 消息上,"never tool results"。tool call 和它的结果必须待在一起,否则重建出来的对话在 provider 那边不合法。
  • 一整轮用户请求超预算时,拆两段摘要再合并。 如果切点落在某个 user-message span 内部(源码里叫 isSplitTurn),Pi 会生成「历史摘要」和「本轮前缀摘要」两份再合并,而不是硬切。
  • 序列化时 tool result 截断到 2000 字符。 TOOL_RESULT_MAX_CHARS = 2000,超出部分替换成 [... N more characters truncated]。因为 read 和 bash 的结果是上下文的绝对大头,不截断的话「用来省上下文的摘要请求」自己就会撑爆上下文。
  • thinking block 不参与摘要。 官方文档给的理由是:provider 返回的 thinking 是推理的摘要,重放它往往不是模型真正的上下文,放进去反而可能让摘要请求比被摘要的内容还大。

默认阈值也写死在源码里:reserveTokens: 16384、keepRecentTokens: 20000,可以按模型覆盖。

溢出恢复的步骤也是个可以抄的设计:先落盘最终回答,触发 turn_end / agent_end,追加 context_edit 的省略记录,需要重试就先跑 session_before_compact 并追加压缩记录,最后把重试当成一次全新的 run 启动。压缩失败或被取消时,省略记录留着、压缩不写、也不排内部重试。

为什么这么做:

一句话:把「上下文管理」从一次不可逆的编辑,降级成一次可回滚的写操作。

顺带的收益是分支几乎免费。有了这条边界,/tree 导航、/fork、/clone 全退化成「把另一个 entry 当成叶子」。切分支前还能让模型总结一下你放弃的那条路(branch summarization),把结论带进新分支。

代价(这一条 Pi 自己也在付):

  • 会话文件只增不减。压缩省的是 token,不是磁盘。
  • 复杂度全压在这一层。compaction.md 这份参考文档有 460 行,光「溢出恢复的顺序」就占一整节;session-manager.ts 超过 2000 行。
  • 结构演进要带迁移。会话格式已经走过 v1(线性)→ v2(树)→ v3(把 hookMessage 角色并入 custom),加载旧会话时自动迁移。
  • 摘要的输出上限曾经是固定的,被 reasoning 吃掉预算后会直接失败。v1.4.0 才改成用模型自己的输出上限,修的就是 generation hit the token cap 这条。

设计亮点二:不做权限系统,只做「加载前的信任」

常规方案:

agent 执行每个工具调用之前弹一个确认框,或者给命令配 allowlist / denylist。看着最安全,实际有两个问题:用户会习惯性按 yes(审批疲劳);而且审批发生在「动作已生成」之后,你能拿来判断的信息只有一句命令行。

Pi 的做法:

明确不内置权限系统。docs/security.md 第一段就写:Pi 会用启动它的账号权限读写文件、执行命令,并且不会在每次工具调用前请求批准。它唯一的内建控制叫 project trust,管的是另一件事——启动时加载哪些资源。

触发信任决策的是一组具体文件:

  • .pi/settings.json、.pi/mcp.json
  • .pi/extensions、.pi/skills、.pi/prompts、.pi/themes
  • .pi/SYSTEM.md、.pi/APPEND_SYSTEM.md
  • 当前目录或祖先目录里的项目 .agents/skills

决策结果存 ~/.pi/agent/trust.json,defaultProjectTrust 默认是 "ask"。有意思的是决策本身可插拔:扩展可以监听 project_trust 事件,第一个返回 yes 或 no 的扩展拥有最终决定权(类型上是 ProjectTrustEventResult,三态 yes | no | undecided)。也就是说,「要不要信任这个目录」也被做成了用户可编程的策略,而不是硬编码的弹窗。

为什么这么做:

因为审批弹窗不是安全边界。真正的边界只有一个问题:生成的动作能碰到什么。 这个问题不该由 agent 在进程内回答——agent 和它执行的命令共享同一套 OS 权限,它没法给自己上锁。所以要靠容器、micro-VM、sandbox 来回答。

Pi 把这件事整理成四种隔离方式,代价写得挺清楚:

  • Plain Docker:整个 Pi 进程进容器,最简单
  • Docker Sandboxes:真凭据留在宿主,由代理替换
  • OpenShell:策略控制的沙箱,管文件、进程、网络、凭据
  • Gondolin 扩展:Pi 留在宿主,只把内置工具和 ! 命令路由进本地 Linux micro-VM,凭据也在宿主

敢承认的地方(这也是我觉得它可信的原因):

security.md 自己承认 project trust「不是一个完整的启动边界」——因为在解析信任之前,Pi 已经读过项目的 sessionDir 配置。它也承认 Gondolin 那种「只隔离工具」是更窄的隔离:宿主上的 Pi 本身和别的扩展仍在边界外。

另一层沙箱在 codemode 里。 这一层不是为了防坏人,是为了收敛「模型写的代码」的影响面:脚本跑在 QuickJS WASM 沙箱里,没有 Node API、没有文件系统、没有网络、没有定时器,只能通过 tools.xxx() 和 models 访问外面;而且只有脚本的输出会进模型上下文。一个几十行的调用循环,最后可能只给模型留三行结论——省上下文和收权限边界,在这里是同一件事。

项目细节补充

供应链被当成代码来审。 一个 agent 会执行模型生成的 npm install,它自己就是高价值攻击面。Pi 的做法是把依赖变更当代码变更对待:直接依赖全部 pin 到精确版本;.npmrc 里 save-exact=true 加 min-release-age=2(不碰当天刚发布的包);CI 用 npm ci --ignore-scripts;pi.dev 安装器不读根 lockfile,而是读一份从根 lockfile 生成的 install-lock/ 来 pin 传递依赖;pre-commit 会拦住 lockfile 提交,除非显式设 PI_ALLOW_LOCKFILE_CHANGE=1;带生命周期脚本的新依赖要先进 allowlist。

模型目录不在仓库里。 models.generated.ts 只是生成产物,真正的 provider 数据目录被 gitignore,构建时 hydrate。直接后果是构建要联网,于是 Nix 那条路又得离线——所以 nix/model-catalog.json 里存了一个 pi.dev 模型目录的 sha256 revision pin,运行时再从 pi.dev 覆盖新数据。

终端这一层是自研的。 pi-tui 没有踩在 Ink 或 blessed 上,它自己实现差分渲染:主屏和备用屏两套渲染器,更新包在 CSI 2026 同步输出序列里避免闪烁,剪贴板和修饰键走原生 prebuild(macOS Objective-C、Linux X11 C、Windows C)。

会话是能直接拿去看的文件。 落在 ~/.pi/agent/sessions/ 下,文件名是「时间戳加 session id」,工作目录编码进上层目录名。加上 JSONL 一条一行的格式,ls、grep、diff 这些老工具直接就能用在 agent 的历史上。

社区规则挺硬。 新贡献者的 issue 和 PR 默认自动关闭,维护者每天翻一遍把值得看的捞出来;被回复 lgtmi 以后 issue 才不被自动关,lgtm 才能提 PR。CONTRIBUTING 里有一条叫 "You must understand your code":用 AI 写代码没问题,提交自己讲不清的 AI 垃圾不行。他们还专门回答了「为什么不让 AI 全自动分级」——AI 可以做去重归纳,最终判断仍然留给人。

两件「原则被现实修正」的记录。 一是改名:旧包 @mariozechner/* 的兼容不是靠别名表,官方公告说是「在我们的 jiti loader 里做了一些魔法」,让老扩展自动指向新包。二是 MCP:据 The Register 报道,Pi 的作者曾认为 MCP 没必要,2026 年 10 月还是加了 MCP 支持,公司的解释是「情况会变」。

项目资源汇总

官方资源

  • GitHub:earendil-works/pi
  • 官网 / 文档:pi.dev | pi.dev/docs/latest
  • 开源协议:MIT
  • 更新日志:packages/*/CHANGELOG.md,每个包一份
  • 贡献指南:仓库内 CONTRIBUTING.md;开发规则在 AGENTS.md
  • npm:@earendil-works/pi-coding-agent
  • 安装脚本:pi.dev/install.sh / pi.dev/install.ps1
  • Nix:nix run github:earendil-works/pi/stable
  • 社区:Discord
  • 长期规划:rfc.earendil.com

相关资源

  • OpenClaw:用 Pi SDK 做的真实集成
  • pi-chat:Slack / 聊天场景的自动化
  • pi-share-hf:把 agent 会话发布到 Hugging Face 的工具(README 在征集公开的 OSS 会话数据)

媒体报道与延伸阅读

核心要点回顾

  • Pi 是 harness 不是产品:11 个包拆开,默认极简,sub-agent 和 plan mode 故意不做,能力靠扩展补
  • 会话是 JSONL 树,压缩只追加一条 CompactionEntry,用 firstKeptEntryId 指回原文边界——历史不删,所以可回退、可分支、可审计
  • 没有内置权限系统。它只做「加载前信任」,把安全边界交给容器 / micro-VM / sandbox,并且在文档里把这条边界划在哪写清楚
  • 扩展是运行时加载的 TypeScript,靠 jiti 免编译;分发走 npm / git,版本 pin 死

适合谁

  • 想把 agent 能力嵌进自己产品、又不想被某个 IDE 绑住的开发者
  • 需要在团队内统一 agent 行为(工具、命令、模型接入)的工程团队
  • 关心上下文管理、会话存储、agent loop 抽象这类底层设计的架构爱好者
  • 习惯在容器里跑 YOLO agent、并且愿意自己写策略的人

一句话评价

它没有把 coding agent 做得更强,而是把它拆得足够开,让你能装成自己要的样子。

← Previous projectGhostty
Next project →uv