我把 pi 的 npm 发布包(
@earendil-works/pi-coding-agent@0.84.4)解开量了三个数字:默认系统提示词 约两千字符 / 28 行;其中 53%–59% 的篇幅在告诉模型「pi 自己的文档和示例在哪」(占比随安装路径长度浮动,第三节给出不变量);官方文档明确列在「我们没做」清单里的权限弹窗,在发布包的示例目录里是一个 34 行的扩展文件。三个数字都是我自己跑出来的(复现命令在文末),它们指向同一个判断:pi 的「极简」不是审美偏好,是预算腾挪——把内核压到最小,腾出的位置用来做一件别的事:让 harness 本身成为 agent 可读可改的对象。
pi 是什么,一句话:一个 MIT 协议的终端编程 agent,跟 Claude Code、Codex CLI 同类。官网(pi.dev)署名 Earendil Inc. & contributors;作者是 Mario Zechner(网名 badlogic,libGDX 作者)、联合创始人含 Flask 作者 Armin Ronacher——这两条作者归属来自二手报道,我没有从官方页面核实到。它的标语是 “There are many agent harnesses but this one is yours” 和 “Primitives, not features”。
这篇不做功能巡览。功能列表官网写得比我清楚。我关心的是一个更根本的问题:
当你说一个 harness「极简」时,省下来的预算花到哪去了? 省下的位置如果只是空着,极简就只是功能少;如果被用来换取另一种能力,极简才是一个设计决策。pi 的答案我量出来了:一半的提示词预算,买的是「agent 能改自己的 harness」。
这是 harness 系列的延续。《什么才是好的 Harness:判断归模型,物理归代码》 讲的是 harness 该做什么;《读 ECC》 讲的是在既有 harness 上叠意见;这篇讲第三条路——把 harness 本身变成可写对象。
名词速查
| 术语 | 一句话解释 |
|---|---|
| harness | 包裹模型的那个有状态程序:决定模型每一步看见什么、输出能对世界做什么(详见旧文) |
| 系统提示词 | 每次请求都塞在对话最前面的那段固定指令,占用上下文预算 |
| JSONL | 一行一个 JSON 对象的文本格式,适合只往后追加 |
| 会话树 | 会话记录不是一条直线,而是每条记录指向上一条,可以从中途分叉出多条并存的路径 |
| steering | agent 干活期间插话:消息排队,等它当前这轮工具调用跑完再送进去 |
| compaction(压缩) | 上下文快满时把前面的历史换成一段摘要,腾出空间(详见旧文) |
| micro-VM | 比容器隔离更强、比传统虚拟机启动更快的轻量虚拟机 |
| jiti | 一个 TypeScript 即时加载器:.ts 文件不用先编译就能被 Node 引入 |
一、先纠一个流传中的数字:不是 4 个工具,是 8 个
我动手前先搜了一圈,二手介绍里最常见的说法是「pi 只有 4 个内置工具:read / write / edit / bash」。这个说法和官方文档冲突。官方 usage 文档原话列的是:read、bash、powershell(仅 Windows)、edit、write、grep、find、ls。
我解开发布包看了 dist/core/tools/ 目录,8 个工具实现文件一个不少:bash、edit、find、grep、ls、powershell、read、write。
那「4 个」这个说法是凭空来的吗?不是。它有一个很具体的出处——系统提示词构造函数里的兜底默认值:
// 真代码,dist/core/system-prompt.js(发布包 0.84.4)
const tools = selectedTools || ["read", "bash", "edit", "write"];
调用方没传工具集时,提示词按这 4 个来渲染。一个内部兜底常量,在传播中变成了产品事实。 这件事本身值得记一笔:agent 工具链的二手介绍失真率很高,涉及具体数字时,解包成本通常只有五分钟。
顺带一个真实的取舍:8 个工具里有 3 个(grep/find/ls)和 bash 功能重叠——bash 本来就能 ls、rg、find。为什么还要专门做?因为专用工具能规范输出(尊重 .gitignore、截断、结构化返回),这正是 ACI 那条线的老结论:接口的形状塑造 agent 的行为,一个设计糟糕的接口比没有这个接口更糟(SWE-agent 的消融实验,我在旧文里抄过表)。pi 没有为了「极简」这个口号砍掉它们。
二、提示词是工具集的函数,不是常量
量到的数字(方法:把 system-prompt.js 的两处 import 换成桩,直接调 buildSystemPrompt,命令见文末):
| 配置 | 字符数 | 行数 | 词数 | token 估算(字符/4,估算不是实测) |
|---|---|---|---|---|
| 默认 4 工具 | 1987 | 28 | 282 | ~500 |
| 7 工具(非 Windows 全量) | 2089 | 30 | 295 | ~520 |
(字符数含三处 pi 安装路径,此处按 12 字符占位符计;真实安装路径更长,见第三节。)
两千字符量级。但比总量更有意思的是它怎么被拼出来的。看这段真代码:
// 真代码,dist/core/system-prompt.js
const hasBash = tools.includes("bash");
const hasGrep = tools.includes("grep");
const hasFind = tools.includes("find");
const hasLs = tools.includes("ls");
// 只有在「有 bash 但没有专用文件工具」时,才教模型用 bash 干这些活
if ((hasBash || hasPowerShell) && !hasGrep && !hasFind && !hasLs) {
addGuideline("Use bash for file operations like ls, rg, find");
}
还有一条注释,把机制说得比文档清楚:
// A tool appears in Available tools only when the caller provides a one-line snippet.
两件事合起来是一个具体的设计选择:
- 每个工具在提示词里只占一行。
read: Read file contents,就这样。工具的完整参数结构走 tool-calling 的 schema 通道(TypeBox 定义),不在提示词里重复。提示词负责「有什么」,schema 负责「怎么调」。 - 规则跟着工具集走。关掉
grep,那条「用 bash 搜文件」的指引才出现;开着grep,它就不出现。
第 2 点是我认为最值得抄走的一条。绝大多数 harness 的系统提示词是一整块静态字符串,工具开关和提示词规则各改各的,于是必然积累僵尸规则——指向已经不存在的工具、或者和当前配置矛盾的句子。模型读到这些句子时不会报错,它会照做,然后失败。pi 把提示词写成工具集的函数,从机制上消掉了这类漂移。
这就是旧文那句「判断归模型,物理归代码」的一个具体落点:「不要提到不存在的工具」这条规则,不该写进提示词请求模型配合,该写成拼装逻辑让它无法发生。
三、省下来的预算,一半买了「agent 能改自己」
现在是这篇的核心数字。我把默认提示词切成两段量——前半是「怎么当一个编程助手」,后半是「pi 自己的文档在哪」。
第一次量的时候我踩了个坑,值得写出来:后半段包含三处 pi 的安装路径,所以它的长度取决于 pi 装在哪。我拿四种路径各量了一遍:
| pi 安装目录(三处路径的公共前缀) | 前半段(编程助手) | 后半段(pi 自述) | 后半段占比 |
|---|---|---|---|
| 整条路径压成 1 字符(绝对下限) | 911 | 1019 | 52.8% |
1 字符目录(X/README.md) | 911 | 1043 | 53.4% |
12 字符占位符(<PI_INSTALL>/README.md) | 911 | 1076 | 54.2% |
/usr/local/lib/node_modules/...(npm 全局装) | 911 | 1217 | 57.2% |
~/.nvm/versions/node/vXX/lib/node_modules/... | 911 | 1295 | 58.7% |
前半段恒定 911 字符(18 行),这是不变量;后半段 1019 字符起,越是真实的安装环境越长。 所以准确的说法是:
教模型「怎么当一个编程助手」用了 911 字符;教模型「怎么查阅并改造 pi 这个 harness」用了 1019 字符以上,在常见安装方式下是 1200–1300 字符。后者更长,而且在真实机器上明显更长。
我导语里写「53%–59%」而不是一个漂亮的单一数字,就是因为这个。路径长度这种实现细节能把占比推高 5 个百分点,把它藏起来只报 54.2% 会显得更精确、但更不诚实。
那 8 行长什么样(节选,安装路径已替换为占位符):
Pi documentation (read only when the user asks about pi itself, its SDK,
extensions, themes, skills, or TUI):
- Main documentation: <PI_INSTALL>/README.md
- Additional docs: <PI_INSTALL>/docs
- Examples: <PI_INSTALL>/examples (extensions, custom tools, SDK)
- When asked about: extensions (docs/extensions.md, examples/extensions/),
themes (docs/themes.md), skills (docs/skills.md), ... TUI components (docs/tui.md) ...
- When working on pi topics, read the docs and examples, and follow
.md cross-references before implementing
把这条和另外三个实现事实放在一起,才看得出它不是随手加的:
- 扩展是
.ts文件,jiti 直接加载,没有构建步骤(官方扩展文档)。放进~/.pi/agent/extensions/或项目里的.pi/extensions/就会被自动发现。 /reload热重载;而pi.registerTool()更进一步,文档说它在运行时(session_start、命令处理器里)注册即生效,连/reload都不用。- 系统提示词自身可被文件替换:项目级
.pi/SYSTEM.md或全局~/.pi/agent/SYSTEM.md整体替换,APPEND_SYSTEM.md追加。
四件事凑成一个闭环:模型知道文档在哪 → 能写 .ts → 不用编译 → 注册即生效。于是「让 pi 给我加个功能」变成一次普通的编码任务,而不是一次二次开发。这就是那过半篇幅买到的东西。
我的判断:这是一个我在别处没见过的预算分配。多数 harness 的系统提示词里,关于「harness 自己」的信息量接近零——因为它默认 harness 是给定的、不可变的。pi 反过来,把「harness 可变」写进了模型每一次请求都要读的那段文字里。
一个怀疑读者会立刻反问:这不就是把成本转嫁给用户吗? 部分是的,第六节我会正面回答。但先看第四节——因为「转嫁」这个说法有一个前提是错的。
四、「我们没做」清单的真相:做成了 34 行的示例
官方 usage 文档的原话是:pi “intentionally does not include built-in MCP, sub-agents, permission popups, plan mode, to-dos, or background bash”。
我数了发布包的 examples/extensions/ 目录:79 个条目。然后把「没做」清单和目录对一遍,wc -l 量行数:
| 官方声称「没做」 | 示例目录里的对应物 | 行数(实测) |
|---|---|---|
| 权限弹窗 | permission-gate.ts | 34 |
| 权限弹窗(危险命令确认) | confirm-destructive.ts | 59 |
| 路径保护 | protected-paths.ts | 30 |
| plan mode | plan-mode/index.ts + utils.ts | 390 + 168 |
| 沙箱 | sandbox/index.ts | 321 |
| 沙箱(micro-VM) | gondolin/ | 目录 |
34 行的权限门,全文如下(真代码,未删减,仅把注释译成中文):
export default function (pi: ExtensionAPI) {
const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "bash") return undefined;
const command = event.input.command as string;
if (!dangerousPatterns.some((p) => p.test(command))) return undefined;
if (!ctx.hasUI) {
// 非交互模式下默认拦截
return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" };
}
const choice = await ctx.ui.select(`⚠️ Dangerous command:\n\n ${command}\n\nAllow?`, ["Yes", "No"]);
if (choice !== "Yes") return { block: true, reason: "Blocked by user" };
return undefined;
});
}
所以「没做」这个词是不准确的。准确的说法是:这些功能在仓库里,但不在内核里,也不默认开启。 被删掉的不是功能,是两样东西——默认值和不可替换性。
这也解释了 pi 真正的产品是什么。不是那 8 个工具,而是扩展点的完备性。看官方扩展文档列的事件面(这些是文档里的标识符,不是我编的):
| 事件 | 能干什么 | 于是「谁」变成了扩展 |
|---|---|---|
context → 返回 { messages } | 每次请求前重写整个上下文 | RAG、长期记忆、自定义压缩 |
before_agent_start | 注入消息、替换 systemPrompt | 人格、模式切换(plan mode) |
tool_call → { block, reason, terminate } | 拦下任意工具调用 | 权限门、路径保护、沙箱 |
before_provider_request / after_provider_response | 改发出去的请求 / 收回来的响应 | 协议适配、注入、审计 |
user_bash | 接管用户敲的 ! 命令 | 把执行搬到别处(远端 / micro-VM) |
session_before_compact / session_before_fork / session_before_tree | 在会话结构变更前介入 | 自定义压缩、检查点、自动提交 |
这张表看下来会有一个感觉:这些 hook 不是「常见需求的便利入口」,是把 agent 循环的每一个接缝都拆出来了。一个 harness 能做的事情,本质上就是「决定模型看见什么」和「决定模型的输出能对世界做什么」——context 管前者,tool_call 管后者,两个 hook 就把这两句话的全部实现空间交出去了。
这里有一条我自己踩过的印证。上周我做把 Claude Code 的后端换成扩散模型那个实验时,卡在协议不兼容上,最后的解法是自己写了一个 HTTP 代理去改请求和响应。before_provider_request / after_provider_response 这两个 hook,做的正是我那个代理干的事——只不过它在进程内,不需要我伪造一个 API 端点。 那次实验的结论是「固定 harness 换模型」这个方向的收益有限;pi 提供的是同一个坐标系的另一根轴。
五、一旦会话是树,状态就必须是会话的函数
这一节是全篇最硬的一条设计推导,也是最可复用的一条——它不属于 pi,属于任何想做会话分叉的 harness。
pi 的会话是 JSONL 文件,一行一个条目,每个条目有 8 字符十六进制 id 和一个 parentId(根为 null)。parentId 指针构成的是树,不是列表。 从一个较早的条目再生一个子节点,就得到一条并存的分叉路径,同一个文件里,不复制文件。
graph TD
A["session 头: version / cwd"] --> B["message: 用户提问"]
B --> C["message: 助手 + toolCall"]
C --> D["message: toolResult"]
D --> E["message: 走错了的那次尝试"]
D --> F["branch_summary (fromId = E)"]
F --> G["message: 换个方案重来"]
G --> H["compaction: summary + retainedTail"]
H --> I["当前 leaf"]
几个具体机制(来自官方 session-format 文档):
- 用
/tree从一条路径切走时,会写一条branch_summary条目,字段fromId指向被放弃的叶子,内容是那条分支「到共同祖先为止」的 LLM 生成摘要。放弃一条路,但不丢弃这条路上学到的东西——这是我觉得最漂亮的一笔。 buildContextEntries()从 leaf 往 root 走,沿路应用 compaction;有retainedTail时它「充当一个自包含的检查点」。/fork是另一件事:生成新文件,头部的parentSession指向原.jsonl。- 格式有三个版本,v1 是扁平列表,v2 加了树,v3 把
hookMessage角色改名custom;旧文件读取时自动迁移到 v3。(注意 v1→v2 这个演进方向:树是后加的,不是一开始就有的。)
现在是推论。官方扩展文档里有一段「状态模式」的建议,第一次读会觉得别扭:
把状态放在工具返回值的
details字段里,然后在session_start时扫描ctx.sessionManager.getBranch()里的toolResult消息重建——这样分支和 fork 才是对的。
为什么不能直接在扩展里用一个模块级变量存状态?因为会话是树。假设你的扩展在内存里记了「已经跑过 3 次测试」,用户 /tree 切到另一条分支——那条分支上只跑过 1 次。内存里的 3 属于进程,不属于当前路径,它现在是错的。
一句话推论,可以带走:
会话从线变成树的那一刻,「当前状态」的定义就从「进程累积到现在的值」变成了「从根到当前叶子这条路径的函数」。任何存在进程内存里的 agent 状态,都会在第一次分叉时失真。
这条对任何做 checkpoint / 回溯 / A-B 重试的 agent 系统都成立,跟 pi 无关。它也解释了为什么 custom(不进上下文)和 custom_message(进上下文)要分成两种条目类型——扩展需要一个「写进会话但不喂给模型」的位置来放自己的状态。
六、权限:pi 承认它没有安全边界
现在回答第三节留下的那个反问。官方安全文档的原话,两条:
- agent「以启动它的用户和进程的权限运行」(runs with the permissions of the user and process that launched it);
- 扩展「以你的完整系统权限运行,可以执行任意代码」(run with your full system permissions and can execute arbitrary code)——所以只装可信来源的扩展。
隔离外推给容器,文档给了三种模式:Gondolin 扩展(把 auth 留在宿主机,内置工具和 ! 命令路由进一个 Linux micro-VM)、朴素 Docker、以及 OpenShell(策略受控沙箱)。
我认为这个取舍在方向上是对的,理由很物理:一个有 bash 工具的 agent,进程内的白名单是纸糊的。你可以拦 rm -rf,模型可以写 python -c "import shutil...";你可以拦 python,它可以写个 shell 脚本再执行。只要执行原语是图灵完备的,进程内的模式匹配就只是提高了绕过的成本,不是边界。真边界在进程外:容器、micro-VM、单独的用户。
但这是灰度,不是黑白。权限弹窗仍然有一个 pi 的说法覆盖不到的价值:它防的不是恶意,是误操作。 模型不是攻击者,它只是有时候会在错误的目录里跑 git clean -fd。弹一次窗让人看一眼,成本极低、收益具体,和「防不住有意绕过」这件事并不矛盾。pi 的回答是「那你装那 34 行」——这个回答成立,但它把一个安全默认值变成了一次安装决策,而没装的人不会知道自己没装。
所以我的实际判断:
- 单人、跑在容器或专用机器上、要深度定制 → pi 的模型是干净的,安全边界外推是正确的工程。
- 团队共用配置、跑在开发者主机上、有合规要求 → 你需要的是「默认安全 + 可关」,不是「默认无边界 + 可加」。这时候 pi 要求你先建立一套自己的基线扩展包,那是真实成本。
顺带一个正面信号:供应链纪律做得比大多数同类工具紧(官方 README)——直接依赖精确锁版本、.npmrc 里 save-exact=true 和 min-release-age=2(依赖发布满两天才允许引入)、生成 npm-shrinkwrap.json、CI 用 npm ci --ignore-scripts、定期 npm audit。文档还特意说明正常 npm 安装不需要 install 脚本,所以推荐带 --ignore-scripts 装。一个把安全边界外推给容器的项目,在供应链这一侧反而是收紧的——这两件事不矛盾,正好说明它对「边界该设在哪一层」有明确观点。
七、并行工具带来的物理约束
一个容易被忽略的实现细节,值得单独提,因为它是「物理归代码」的教科书例子。
pi 的工具是并行执行的。文档直接把风险写出来了:多个工具同时改一个文件,「whichever write lands last overwrites the other」(最后落地的那个写覆盖掉另一个)。解法是要求改文件的工具把读-改-写包进 withFileMutationQueue(absolutePath, fn)。
实现只有几十行,核心逻辑是:
// 伪代码,对应 dist/core/tools/file-mutation-queue.js 的 withFileMutationQueue 主干
export async function withFileMutationQueue(filePath, fn) {
// 用 realpath 做 key:软链接、相对路径指向同一文件时归为同一把锁
const key = await realpath(resolve(filePath));
const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
// 把自己接到这个文件的队尾,形成 promise 链
fileMutationQueues.set(key, currentQueue.then(() => nextQueue));
await currentQueue; // 等前面的人改完
try { return await fn(); }
finally { releaseNext(); } // 放行下一个;省略:并发注册串行化与队列清理
}
源码注释里那句话是关键:「同一文件的改动串行化,不同文件仍然并行」。用 realpath 作 key 而不是原始字符串,处理的是软链接和相对路径指向同一文件的情况——这种细节是踩过才会写的。
为什么这算「物理」?因为它不是一条提示词规则(「请不要同时编辑同一个文件」),而是一个模型无法违反的串行化原语。模型可以并发发起十个 edit,落到同一个文件上时它们自动排队。判断(改哪里)归模型,物理(不许互相覆盖)归代码。
八、什么时候不该选它
灰度部分,直说。
pi 适合:想完全控制 agent 行为的人;要把 agent 嵌进自己产品的人(它有 SDK / RPC / JSON 事件流三种非交互模式);多 provider 多模型切换的人;受不了 harness 每次更新就变个行为的人——作者的公开动机正是这条(据二手报道,他 2025 年 4 月起用 Claude Code,欣赏它但受不了快速迭代带来的行为不稳定;这条我未从官方页面核实)。
pi 不适合:
- 不写 TypeScript 的人。这不是小门槛。pi 的价值有一大半锁在扩展层,不写扩展的话,你拿到的是一个功能比同类少的 CLI。
- 需要开箱即用的团队基线。见第六节。
- 重度依赖 MCP 生态的人。MCP 不在内核,是扩展;生态兼容性的验证成本归你。
- 把「稳定」理解成「我不用管」的人。pi 给的稳定是「行为由你的配置决定,不由上游更新决定」——这是把变更控制权连同维护责任一起交给你。你自己的扩展会在 pi 升级时坏掉,那时没有上游帮你修。
第 4 点值得多说一句,因为它是这类设计的通用代价:可定制性的账,最终都记在维护上。 ECC 那篇里我说过类似的话——把意见装箱发货,好处是别人能直接用,坏处是箱子里的东西要有人一直修。pi 把箱子换成了一套接口,接口比实现稳定,但接口之上的实现依然要有人修,那个人是你。
九、带得走的东西:四个可以问任何 harness 的问题
pi 的四个设计选择,每一个都能翻译成一个体检问题。这不是什么「定律」,是一份自检表——我打算拿它去量我用的每一个 harness。
| # | 问题 | pi 的答案 | 为什么这个问题有信息量 |
|---|---|---|---|
| 1 | 系统提示词是常量还是当前配置的函数? | 函数(工具集变,指引跟着变) | 静态提示词必然积累僵尸规则,模型不会报错,它会照做然后失败 |
| 2 | 「决定模型看见什么」这一步,有没有外部可介入点? | 有(context 事件返回 { messages }) | 没有这个点,RAG / 记忆 / 自定义压缩就只能靠 fork 源码 |
| 3 | agent 状态存在进程里还是会话路径里? | 会话路径(details + 扫 getBranch() 重建) | 只要支持分叉/回溯,存进程内存的状态一定会失真 |
| 4 | 安全边界画在进程内还是进程外? | 进程外(容器 / micro-VM),且明说了 | 有 bash 的 agent,进程内白名单是纸糊的;说清楚比假装有边界诚实 |
如果只带走一句:
看一个 harness 的取舍,不要看它的功能表,看它的提示词预算表。 提示词是每次请求都付一遍的固定成本,是这个系统里最贵的地皮。谁占了这块地,就是这个 harness 真正想让模型做的事。pi 在这块地皮上用一半面积盖了「pi 自己的说明书」——它想让模型做的事,是改造 pi。
诚实的提醒
我亲手测出来的(发布包 @earendil-works/pi-coding-agent@0.84.4,解包 1044 个文件):8 个工具实现文件;提示词兜底默认值是那 4 个工具;提示词非自述部分恒为 911 字符 / 18 行,自述部分 1019–1295 字符(随安装路径长度变化),占比 52.8%–58.7%;examples/extensions/ 79 个条目;permission-gate.ts 34 行、plan-mode/index.ts 390 行、sandbox/index.ts 321 行。
一处我自己改过的结论:初稿我按 12 字符占位符只报了「54.2%」这个单一数字,跑复现脚本时才发现占比对安装路径长度敏感(最高到 58.7%)。所以正文改成了区间加不变量。留这句在这里是因为它就是「手算数字先跑一遍」这条纪律的实际收益——如果我没跑那三分钟的复现,这篇会带着一个看起来很精确、但隐藏了 5 个百分点浮动的数字发出去。
有来源但我没亲手复现的:会话树的分叉行为、branch_summary 的实际生成内容、各 hook 的运行时语义、三种容器化模式的效果——这些都来自官方文档,我没有实际跑一个 pi 会话去验证。我也没有装 pi 跑任何编码任务,所以这篇不含任何性能或好用程度的判断。
明确标注为估算:token 数是字符数除以 4 的粗估,不是真 tokenizer 的结果。字符数是硬数字,token 数不是。我也故意没有引用 Claude Code 的系统提示词长度来对比——我拿不到它的一手数字,而拿逆向版本的数字和实测数字并列,是在制造虚假的可比性。这一节的论点不依赖跨产品对比:54.2% 这个内部占比自己就说明了预算分配。
两处二手信息我标了未核实:作者身份(Mario Zechner / Armin Ronacher)和创作动机。另外我抓到的 GitHub star 数在两个来源里差了一倍(6.2 万 vs 9.99 万),所以这篇一个 star 数都没用——热度不是论据。
成本最低的亲手验证实验(约 3 分钟,不需要安装 pi,只在临时目录里解包一个 npm 包):
# 1. 解包发布包到一个临时目录
TMP=$(mktemp -d) && cd "$TMP"
curl -sL -o pi.tgz "$(npm view @earendil-works/pi-coding-agent dist.tarball | tr -d "'")"
tar xzf pi.tgz && cd package
# 2. 核对工具数与示例数
ls dist/core/tools/*.js | grep -Ev "diff|queue|utils|index|accumulator|truncate|wrapper|path-"
ls examples/extensions | wc -l
wc -l examples/extensions/permission-gate.ts
# 3. 把 system-prompt.js 的两处 import 换成 1 字符路径桩,直接调用它量提示词
sed -e 's#^import { getDocsPath.*#const getReadmePath=()=>"X",getDocsPath=()=>"X",getExamplesPath=()=>"X";#' \
-e 's#^import { formatSkillsForPrompt.*#const formatSkillsForPrompt=()=>"";#' \
dist/core/system-prompt.js > sp.mjs
node --input-type=module -e '
import { buildSystemPrompt } from "./sp.mjs";
const p = buildSystemPrompt({ selectedTools:["read","bash","edit","write"],
toolSnippets:{read:"Read file contents",bash:"Execute bash commands (ls, grep, find, etc.)",
edit:"Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
write:"Create or overwrite files"},
promptGuidelines:["Use read to examine files instead of cat or sed.",
"You can inspect PI_* environment variables for current model and session details.",
"Use edit for precise changes (edits[].oldText must match exactly)",
"Use write only for new files or complete rewrites."],
cwd:"/work", contextFiles:[], skills:[] });
const i = p.indexOf("Pi documentation");
console.log("total", p.length, "| non-docs", i, "| self-docs", p.length - i,
"| share", ((p.length-i)/p.length*100).toFixed(1)+"%");'
在 0.84.4 上,第 3 步应当逐字打印:total 1930 | non-docs 911 | self-docs 1019 | share 52.8%——这是把整条路径压成一个字符的绝对下限。把三个 "X" 换成你机器上 pi 的真实安装路径,占比会升到 57%–59%,而 non-docs 911 不动。版本也在往前走,0.84.4 之后数字会变;变的是数字,不变的是「量一下」这个动作只要三分钟。
参考来源
工程实践 / 官方文档
- pi.dev — 官网,设计主张与「我们没做什么」清单
- pi 文档:Usage — 内置工具清单、SYSTEM.md / APPEND_SYSTEM.md、steering、AGENTS.md
- pi 文档:Extensions —
ExtensionAPI、registerTool、完整事件面、状态模式、并行写入警告 - pi 文档:Session Format — JSONL 条目类型、
parentId树、branch_summary、SessionManagerAPI - pi monorepo(GitHub) — 包结构、容器化三模式、供应链纪律
- 发布包
@earendil-works/pi-coding-agent@0.84.4(npm)— 本文所有一手数字的来源
本站相关
- 什么才是好的 Harness:判断归模型,物理归代码 — harness 的三件本职工作与 ACI 消融证据
- 读 ECC:一个仓库把 Claude Code 的 harness 意见装箱发货 — 另一条定制路线:在既有 harness 上叠意见
- 我把 Claude Code 的模型换成了扩散模型 — 固定 harness 换模型的对照实验
- Claude Code 与 Codex 的上下文驱逐 — compaction 与上下文预算
- LLM 为什么会伸手去拿工具 — 工具调用的机制侧