本文是「Pi 源码拆解」系列第 1 篇。系列目录:
- 2026
- 06-19 Pi 源码拆解(一):Coding Agent Harness 的包边界与扩展机制(本篇)
我此前写过 Claude Code 源码拆解系列。Claude Code 将权限系统、MCP、子代理、plan mode 和后台任务等能力集成到 harness 中。Mario Zechner(badlogic)的 pi 采用了另一种范围划分:README 的 Philosophy 小节列出不提供权限弹窗、MCP、子代理、plan mode、内置 to-do 和后台 bash 的决定;每项都有理由和替代实现路径。
两者都是 coding agent harness,差异在于产品策略由哪里实现。Claude Code 将更多策略作为内置功能提供,pi 则由宿主、扩展和外部工具处理部分策略。这个对比有助于区分运行时机制与产品策略。
本文分析 pi 的包分层及其在代码中的实现。后续文章将分别讨论 agent 运行时、会话与上下文管理、客户端/服务端拆分、终端 UI 和 provider 适配层。
包依赖与策略实现位置
pi 的 monorepo 主线包含四个包,依赖关系如下:
@earendil-works/pi-ai:统一多 provider 的 LLM API。@earendil-works/pi-agent-core:agent 运行时,依赖 pi-ai。@earendil-works/pi-coding-agent:CLI 本体,依赖 agent-core、pi-ai 和 pi-tui。@earendil-works/pi-tui:终端 UI 库,不依赖上面任何一个包。
pi-tui 不依赖 agent 或 LLM,coding-agent 同时依赖 agent-core 和 pi-tui,因此 pi-tui 可用于其他终端应用。仓库还包含 protocol、server、client、storage 等客户端/服务端拆分包,它们均声明为 experimental。
这些包通过分层区分执行机制和产品策略;默认模型、压缩时机等可变决策由组合层处理:
agent 核心不依赖 Node。packages/agent/src/agent-loop.ts 全文 792 行,整个 src/ 里只有 harness/env/nodejs.ts 一个文件 import 了 node: 模块;Node 相关能力从 ./node 这个单独出口导出(packages/agent/package.json 的 exports 字段)。因此核心循环可以在非 Node 环境运行。
agent 核心不内置 provider。packages/agent/src/stream-fn.ts:5-10 的注释写明:宿主可以在这里安装默认 stream 函数,「without making pi-agent-core depend on a provider catalog or compatibility layer」。没注入时 getDefaultStreamFn() 直接抛错。默认 provider 由宿主决定。
agent 核心不执行自动 compaction。shouldCompact 在 agent 包里只是个纯函数(packages/agent/src/harness/compaction/compaction.ts:263),输入 token 数和设置,返回布尔值。真正在跑的过程中检查并触发压缩的代码在上层 CLI(packages/coding-agent/src/core/agent-session.ts:2038)。阈值、摘要 prompt 和压缩后保留的消息数量由产品层决定;运行时提供判断与压缩执行函数。
pi-ai 采用相同的接口边界:模型 catalog 只收录支持 tool calling 的模型(packages/ai/scripts/generate-models.ts:1105,tool_call !== true 的直接跳过)。37 家 provider 共享 api 目录下约 10 个接口协议实现,xAI、Groq、OpenRouter 都复用 openai-completions(packages/ai/src/providers/xai.ts:8、groq.ts:6、openrouter.ts:7)。provider 适配细节将在后续文章说明。
从依赖关系和调用位置看,pi 按策略实现位置分层。运行时不决定默认模型、压缩时机或命令审批;宿主、上层 CLI 或扩展提供这些决策,底层负责执行机制。
未内置能力的替代实现与限制
README 的 Philosophy 清单位于 packages/coding-agent/README.md:491-507,包含六项:
- No MCP. 想要的话建 CLI 工具配 README(走 Skill 机制),或者自己写扩展接 MCP。这条附了作者一篇博客的链接,论点是模型本来就会跑命令行,给它一个带文档的 CLI 比维护一个 MCP server 更通用。
- No sub-agents. 替代路径是用 tmux 起多个 pi 实例,或者用扩展自己实现,或者装一个按你的方式做这件事的包。
- No permission popups. 跑在容器里,或者用扩展建符合自己环境要求的确认流程。
- No plan mode. 把计划写进文件,或者用扩展实现。
- No built-in to-dos. 原话是 “They confuse models.” 用 TODO.md 文件代替。
- No background bash. 原话是 “Use tmux. Full observability, direct interaction.” 后台任务输出保留在 tmux 中,可通过终端直接查看和干预,而非存放在 harness 的后台任务队列中。
这些能力的替代实现通常是文件、扩展、外部工具或第三方包。pi 不将其作为内置产品功能提供。以下三个官方扩展示例说明:扩展通过注册事件、命令或工具,将产品策略接入运行时。
**权限确认通过工具执行前事件实现。**扩展订阅工具执行前事件,在命中风险命令时显示确认界面;无交互界面时默认拒绝:
const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i];
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "bash") return;
const command = event.input.command as string;
if (!dangerousPatterns.some((pattern) => pattern.test(command))) return;
if (!ctx.hasUI) {
return { block: true, reason: "Dangerous command blocked" };
}
const choice = await ctx.ui.select(`Dangerous command: ${command}`, ["Yes", "No"]);
if (choice !== "Yes") return { block: true, reason: "Blocked by user" };
});这段代码来自 examples/extensions/permission-gate.ts。tool_call 发生在工具执行之前,返回 { block: true, reason } 即可中止本次调用。
**Plan mode 由扩展状态和事件策略实现:**扩展注册 /plan 命令切换状态,并在计划状态下只允许白名单中的 bash 命令。
pi.registerCommand("plan", {
description: "Toggle plan mode (read-only exploration)",
handler: async (_args, ctx) => togglePlanMode(ctx),
});
pi.on("tool_call", async (event) => {
if (!planModeEnabled || event.toolName !== "bash") return;
const command = event.input.command as string;
if (!isSafeCommand(command)) {
return {
block: true,
reason: `Plan mode: command blocked (not allowlisted). Command: ${command}`,
};
}
});完整示例还会禁用 edit 和 write,并通过 before_agent_start 向上下文加入只读约束;这些行为都位于 examples/extensions/plan-mode/index.ts,而不是核心运行时。
**待办列表作为扩展工具注册,供模型调用。**它的状态从会话中的历史工具结果重建,避免为了一个待办功能向核心会话模型增加字段:
pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
pi.registerTool({
name: "todo",
label: "Todo",
description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
parameters: TodoParams,
async execute(_toolCallId, params) {
if (params.action === "add") {
const newTodo = { id: nextId++, text: params.text!, done: false };
todos.push(newTodo);
return {
content: [{ type: "text", text: `Added todo #${newTodo.id}: ${newTodo.text}` }],
details: { action: "add", todos: [...todos], nextId },
};
}
// list、toggle、clear 分支省略
},
});examples/extensions/todo.ts 将待办数据写入工具结果的 details,并在 session 事件中重新构造状态。核心只需要提供工具注册和会话访问接口。
权限弹窗不作为内置功能提供,但代码向扩展提供了工具调用拦截入口。bash 工具的执行入口经 ops.exec() 抽象调用(packages/coding-agent/src/core/tools/bash.ts:429);未注入自定义 operations 时,默认本地后端才会在 createLocalBashOperations() 中直接调用 spawn(bash.ts:97)。edit 和 write 直接写盘,整条路径上没有任何审批调用。全仓库唯一的拦截点是扩展事件 tool_call(packages/coding-agent/src/core/extensions/types.ts:899),注释写明 “Fired before a tool executes. Can block.”,handler 返回 block 加 reason 就能拦下这次调用,事件里的 input 还可变,扩展可以在执行前改写工具参数。
权限控制的替代方案分为两层。一层是 OS 级隔离,根 README 的 “Permissions & Containerization” 小节指向 packages/coding-agent/docs/containerization.md,给了三条路径:Gondolin 扩展把内置工具和命令路由进本地 micro-VM、整个进程跑进纯 Docker、或者用 OpenShell 的策略沙箱。另一层是可编程性:examples 目录里有 permission-gate.ts 和 protected-paths.ts 两个官方示例,用上面的 tool_call 事件实现项目自己的权限规则。公司内网、个人机器和 CI 容器的规则不同,策略由运行环境和扩展选择。
工具集也遵循这一边界。默认只有 4 个工具:read、bash、edit、write(packages/coding-agent/src/core/tools/index.ts:138-145)。grep、find、ls 可选,其中 grep 和 find 底层调系统的 ripgrep 和 fd,机器上没有时 ensureTool 会自动下载对应二进制(packages/coding-agent/src/utils/tools-manager.ts:326),ls 是纯 Node 实现。系统提示词全文 162 行,完全动态生成(packages/coding-agent/src/core/system-prompt.ts):工具列表只列调用方提供了 snippet 的工具,guidelines 按工具组合条件生成,比如只在「有 bash 但没有 grep/find/ls」时才加一条「Use bash for file operations like ls, rg, find」。项目里的上下文文件(AGENTS.md 之类)以 <project_context> 块追加在末尾,调用方还可以通过 promptGuidelines 和 appendSystemPrompt 注入自己的规则,提示词的每一节都有明确的来源。
这种设计有相应限制:使用者需要自行开发、组合或安装扩展和 skill;团队环境没有统一的安全策略入口,各成员的扩展配置可能不同,管理员无法集中收紧策略。README 还在 packages/coding-agent/README.md:408 明确警告:pi packages 以完整系统权限运行,扩展可执行任意代码,安装第三方包前需要审查其源码。因此,安全策略由使用者和运行环境负责。
Extensions、Skills 与 Prompt templates
pi 通过 Extensions、Skills 和 Prompt templates 提供文件驱动的扩展点,用于补充未内置的能力。
Extensions 是同进程的 TypeScript 模块,用 jiti 在运行时编译加载(packages/coding-agent/src/core/extensions/loader.ts:17)。最小扩展可以只注册一个工具:
import { Type } from "@earendil-works/pi-ai";
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
const helloTool = defineTool({
name: "hello",
description: "A simple greeting tool",
parameters: Type.Object({ name: Type.String() }),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: { greeted: params.name },
};
},
});
export default function (pi: ExtensionAPI) {
pi.registerTool(helloTool);
}该示例来自 examples/extensions/hello.ts。扩展模块在默认导出函数中接收 ExtensionAPI,通过 registerTool 将工具接入模型调用与 TUI 渲染流程。pi 也发布单文件 Bun 二进制,其中无法解析 node_modules。loader 静态 import typebox、pi-agent-core、pi-ai、pi-tui 和 pi-coding-agent 本体,再通过 VIRTUAL_MODULES(loader.ts:50-74)将打包后的依赖提供给 jiti。扩展中 import "@earendil-works/pi-ai" 得到二进制内的同一份模块,扩展与宿主共享类型和运行时状态,避免依赖出现双实例。
Skills 走 agentskills.io 规范,一个 skill 的最小文件是带 YAML frontmatter 的 SKILL.md:
---
name: dynamic-resources
description: Example skill loaded from resources_discover
---
# Dynamic Resources Skill
This skill is provided by the dynamic-resources extension.该文件来自 examples/extensions/dynamic-resources/SKILL.md。formatSkillsForPrompt(packages/coding-agent/src/core/skills.ts:335-361)只将 frontmatter 中的 name、description 和文件 location 以 <available_skills> 写入系统提示词,正文不进入常驻上下文。模型判断任务匹配后,再用 read 工具加载 SKILL.md。这样每轮请求无需携带 skill 正文;agent-skills 那篇讨论了相同机制。Prompt templates 是第三类扩展点,markdown 文件使用同一套资源加载管线,内容更轻。
这三类扩展产物通过 pi packages 分发。包用 package.json 中的 pi 字段声明各类资源目录:
{
"name": "my-pi-package",
"keywords": ["pi-package"],
"pi": {
"extensions": ["./extensions"],
"skills": ["./skills"],
"prompts": ["./prompts"],
"themes": ["./themes"]
}
}安装命令接受 npm、Git 和本地路径来源,并按安装范围写入用户或项目设置:
pi install npm:@foo/pi-tools
pi install git:github.com/user/repo@v1
pi install ./relative/path/to/package -l-l 表示项目级安装;省略时写入用户级设置。包管理器解析来源类型后,分别调用 npm 安装、Git 克隆或本地路径校验,并将成功安装的来源持久化到对应设置(packages/coding-agent/src/core/package-manager.ts:978-1005)。README 在 Philosophy 开头说明:其他工具内置的功能可在 pi 中由扩展或 skill 实现,也可通过第三方包安装。
内置工具和扩展工具都实现 ToolDefinition(packages/coding-agent/src/core/extensions/types.ts:449)。接口除参数 schema 和 execute 外,还包括供系统提示词引用的 promptSnippet(:457),以及工具 TUI 渲染使用的 renderCall(:489)和 renderResult(:492)。扩展工具因此可以接入与内置工具相同的提示词和终端渲染接口。
system-prompt.ts 中的文档路径支持按需读取。默认提示词由函数动态拼接,包含角色、当前可用工具、按工具组合生成的规则和 pi 文档索引。省略运行时替换的工具说明和绝对路径后,固定部分如下:
You are an expert coding assistant operating inside pi, a coding agent harness.
You help users by reading files, executing commands, editing code, and writing new files.
Available tools:
- <tool name>: <one-line snippet>
Guidelines:
- <tool-dependent guideline>
- Be concise in your responses
- Show file paths clearly when working with files
Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
- Main documentation: <README absolute path>
- Additional docs: <docs directory absolute path>
- Examples: <examples directory absolute path>
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
- Always read pi .md files completely and follow links to related docs其中 <tool name> 仅列出调用方选中且提供 toolSnippets 的工具;默认工具集合是 read、bash、edit、write。提供 read 工具时,提示词末尾还会追加 <available_skills>,其中只包含每个 skill 的名称、描述和路径。随后依次追加 <project_context>、可选的 appendSystemPrompt 和当前工作目录。扩展 API 不会全文写入系统提示词,相关信息通过文档索引按需读取。
project trust 与扩展加载配套。项目目录中的 .pi 设置和项目级扩展可以执行任意代码,因此 pi 首次进入目录时会询问「Trust project folder?」,并将结果记录在 trust.json(packages/coding-agent/src/core/trust-manager.ts:212)。信任前只加载用户级扩展,使其能够处理 project_trust 事件。供应链信任处理是否信任项目代码,工具执行权限处理是否允许执行某条命令。pi 只处理供应链信任;工具执行权限由运行环境和扩展负责。
TUI 的渲染方式
pi-tui 是独立的终端 UI 库,组件接口为 render(width): string[](packages/tui/src/tui.ts:29),组件只产出字符串行。渲染器对行数组做 diff,只重绘变化区间;默认交互界面渲染到 main screen,历史进入终端滚动区,也提供可显式启用的 alternate screen 实现。所有更新封装在 CSI 2026 同步输出序列中,避免撕裂。工具的 renderCall/renderResult 返回这些组件,因此可接入扩展体系。差分渲染的实现细节将在其他文章讨论。
后续分析范围
本文涉及 agent 控制流、会话状态、provider 适配与终端呈现等运行时职责。pi 将其中多项策略交由宿主、扩展和外部工具提供。下一篇分析 agent 控制流如何将失败编码为 assistant 消息;后续文章将讨论会话与上下文管理、客户端/服务端拆分、终端 UI 和 provider 适配层。
