一句话结论
一个 coding agent 的「最小内核」不需要框架:767 行 TypeScript、零 npm 运行时依赖,就能写出会读文件、改代码、跑命令的 agent 循环。pi-from-scratch(MIT,2026-08-09 新建,截至 2026-08-13 约 746★)把 pi agent 的数据流拆成可逐行阅读的最小实现——它的价值不是「又一个 agent」,而是把 agent 循环里最容易做错的三个细节写成了显式代码。
项目定位:拆 pi 的数据流,而不是造新框架
README 明说:项目沿 pi(github.com/earendil-works/pi,本稿未读取该仓库)的数据流拆解,「删除 pi 的工程细节,留下 pi 的核心思想」。配套教学网站(pi-from-scratch.vercel.app),文章与源码同屏,右侧编辑器随阅读逐步补全代码,并带 Trace 跟踪可打断点逐行过代码;线上 trace 是预生成静态数据,浏览不发起模型请求。
运行要求:Node.js 22+ 与 OpenAI 兼容 API;NANOPI_API_KEY 必填,NANOPI_MODEL 默认 glm-5.2,NANOPI_BASE_URL 默认 api.openai.com/v1。
「600 行」声明复算:5 个源文件(agent.ts 174 / llm.ts 268 / tools.ts 125 / cli.ts 112 / tui.ts 88)共 767 行,剔除注释与空行为 616 行——声称基本属实(误差约 3%)。package.json 的 dependencies 为空对象,运行时只用 Node 内置能力(fetch、fs、child_process、readline),这是「最小」最有说服力的部分。
最小闭环:一个 while(true)(src/agent.ts:80-174)
循环结构(agent.ts:89-173):每轮先做可选压缩,然后流式请求 LLM 并收集 text 与 tool_calls;把 assistant 消息写回 context;有 tool_call 就串行执行、把 tool_result 写回 context,进入下一轮;模型不再调工具(end_turn)即结束。没有 max_steps——模型说停才停(agent.ts:73 注释:pi 同样无硬编码步数上限,另有 shouldStopAfterTurn 回调,教学版省略)。
上下文压缩(agent.ts:37-70):消息超过 50 条时,把最旧的 30 条序列化成文本、用一轮非流式调用让 LLM 总结,用摘要替换旧消息并保留最近 20 条。两个边界值得注意:压缩失败时保留原始上下文(agent.ts:62-63,注释原话「保留原始消息比空摘要更安全」);abort 期间不压缩(agent.ts:38)。pi 的压缩实现据源码注释约 970 行(含 token 估算/cut point 边界/split turn),教学版用「消息条数」近似——有意识的简化,注释写明了。
三个生产级陷阱的显式化
-
max_tokens 截断守卫(agent.ts:125-135):finish_reason 为 length 且存在 tool_call 时,参数可能是半截 JSON。教学版选择不执行,而是把 "output truncated by max_tokens, args may be incomplete" 作为 tool_result 写回上下文,让模型重试。这个分支直接决定了「截断会不会静默产生错误调用」。
-
abort 的一致性(agent.ts:107-111、165-169):被中断时丢弃已收集的 tool_calls(agent.ts:108 注释:无对应 tool_result 会导致 session 恢复后 API 报错),同时对已发出的每个 tool_call 补一个 "error: aborted" 结果——OpenAI 协议要求 tool_call 与 tool_result 1:1 对应。
-
压缩失败回退(agent.ts:62-63):摘要失败时整个压缩被跳过,原上下文原样保留——空摘要比超长上下文更危险。
这三个分支都是「自己写 agent 时会踩的坑」:截断时执行半截参数、abort 后 API 400、压缩把上下文清空。教学版把它们写成 if 分支。
协议层:OpenAI 兼容 SSE 的正确姿势(src/llm.ts)
- tool_call 增量按 index 累积,arguments 是分段 partial JSON,流结束时按 index 排序统一 flush(llm.ts:122-156)。
- finish_reason 映射:tool_calls → tool_use,length → max_tokens,stop 走默认 end_turn(llm.ts:136-138)。
- 消息格式怪癖(llm.ts:78-89):OpenAI 要求 assistant 消息 content 非 null 或有 tool_calls;纯 tool_call 时 content 为 null,两者皆空用空串占位防 400;tool_result 块必须转成独立的 role:"tool" 消息。
- Context 是纯 JSON(llm.ts:31-35),可整体 stringify 落盘——session 持久化因此只有约 20 行代码(cli.ts:81-104:逐行 JSONL 追加、加载时跳过损坏行而非丢弃全部历史)。
工具层:4 个内置工具与输出治理(src/tools.ts)
read_file / write_file / edit / run_bash(tools.ts:123-125)。细节:所有工具输出统一截取最后 200 行、全文落盘 /tmp(tools.ts:24-31,注释原话「尾部优先——错误信息通常在末尾」);edit 要求 old_string 唯一匹配(tools.ts:86-89)并用函数替换避免 $ 特殊字符被 String.replace 解释;run_bash 30 秒超时、1MB maxBuffer、支持 abort 信号(tools.ts:111)。
零依赖的代价:权限与校验
教学版最值得注意的不是它有什么,而是它明说砍掉了什么(agent.ts:146、tools.ts:3-4 注释):工具参数不验证、run_bash 执行任意命令且没有任何人工批准门。对一个会跑 shell 命令的 agent,「最小闭环」的定义里没有权限层——教学版把它显式排除,恰好说明生产 agent 的权限系统(批准、沙箱、参数校验)是独立于循环本身的一层。读这个项目时对照自己 harness 的权限设计,是最有收获的姿势。
与 pi-textbook 及生产 harness 的差异
07-27 发过的 pi-textbook 是社区课程(15 个 checkpoint 的学习路径,非官方 Pi);pi-from-scratch 是可直接运行的最小实现——前者讲「怎么学」,后者给「能跑的代码」。与 Claude Code / pi / Codex 这类生产 harness 相比,它缺少:权限与批准、参数校验、并行工具执行、token 级压缩、会话树与观测——这些恰好是「生产化」的全部工作。
适用与限制
- 适合:想搞懂 agent 循环/工具协议/上下文压缩的开发者;想写轻量 agent 或做教学的人;对照理解自己使用的 harness。
- 不适合:需要生产可用 agent 的场景(无权限门、无校验);不接受把 API key 发往第三方 base_url 的场景;非 TypeScript 技术栈。
- 限制:未实测(本稿只读源码,未安装未运行);「能跑」的证明是仓库自带 vitest 测试(test/ 目录含 agent/cli/llm/tools/tui/e2e 六个测试文件),本稿未执行;star 数为 GitHub API 2026-08-13 查询。
未实测声明
本文全部基于逐行源码阅读(5 个源文件全文)与仓库公开信息;未安装、未运行 pi-from-scratch,未执行其测试,未读取 earendil-works/pi 仓库与 web/ 目录源码。
来源
- 仓库:github.com/SaladDay/pi…
- agent 循环:github.com/SaladDay/pi…
- 协议层:github.com/SaladDay/pi…
- 工具层:github.com/SaladDay/pi…
- 依赖清单:github.com/SaladDay/pi…
- 教学网站:pi-from-scratch.vercel.app
- 拆解对象(本稿未读取):github.com/earendil-wo…