它是什么
先把身世说清楚:这是一个 monorepo。读 monorepo 的正确方式是读层次——9 个 npm 包彼此正交,靠显式契约粘合。
9 包 monorepo 的解剖
打开仓库第一眼就该注意到的事:根目录有 9 个 packages/ 子目录,彼此通过 "workspace:*" 互相依赖。Pi 不是把 Agent 塞进一个进程——它把 Agent 拆成 9 个独立 npm 包(其中 packages/storage/ 本身不是包,真正的存储包在 packages/storage/sqlite-node/),每个可以单独发布、单独嵌入。
pi-agent-core(62 文件)+ pi-ai(305 文件)—— 纯 SDK,无 UI、无 CLI、可被任何宿主嵌入。pi-coding-agent(488 文件)+ pi-tui(74 文件)—— 终端 CLI + 差分渲染 UI。pi-protocol(12 文件)+ pi-server(38 文件)+ pi-client(18 文件)—— CBOR 二进制线协议,让进程解耦。pi-storage-sqlite-node(12 文件,可选 SQLite SessionStore;CLI 默认 JSONL)+ pi-evals(14 文件,Agent 行为回归评测)。数字化的项目形状
| 字节/行 | 文件 | 是什么 |
|---|---|---|
| 1 351 行 | packages/ai/src/api/anthropic-messages.ts | 最大单文件——Anthropic API 完整适配(cache_control / thinking / copilot 头) |
| 792 行 | packages/agent/src/agent-loop.ts | 双层 while 主循环(外层 follow-up + 内层 tool-call) |
| 577 行 | packages/agent/src/agent.ts | 有状态 Agent 类(队列 + 生命周期) |
| 1 185 行 | packages/agent/src/harness/agent-harness.ts | 上层 AgentHarness(Session 树 + compaction + skills) |
| 3 332 行 | packages/coding-agent/src/core/agent-session.ts | AgentSession(coding-agent 与 agent-core 的桥梁) |
| 1 223 行 | packages/tui/src/tui.ts | TUI 基类(差分渲染引擎) |
| 1 401 行 | packages/tui/src/keys.ts | 键盘处理(传统转义序列 + Kitty 协议) |
| 487 行 | packages/coding-agent/src/core/skills.ts | Skills 系统(agentskills.io 规范) |
| 713 行 | packages/coding-agent/src/core/extensions/loader.ts | Extensions 动态加载器(jiti) |
读完这份报告,你会看到 三个在前面所有 Agent 项目里都没有的设计:自扩展 Extensions、协议驱动进程分离、差分渲染 TUI。这三个加起来,让 Pi 成为"Agent 工程的另一种可能性"。
38 Provider 的统一接口
Pi 支持 38 个 Provider(OpenAI/Anthropic/Google/Bedrock/DeepSeek/Groq/Kimi/Moonshot/ZAI/Xiaomi…),但不依赖 Vercel AI SDK。它的 pi-ai 包是完全独立实现——每个 Provider 的 SDK 适配自己写,主 bundle 不含任何 SDK。
| 抽象层级 | 位置 | 做什么 |
|---|---|---|
| KnownApi | packages/ai/src/types.ts:16-26 | 10 个 API 协议枚举(openai-completions / openai-responses / anthropic-messages / bedrock-converse-stream / google-generative-ai…) |
| Provider<TApi> | packages/ai/src/models.ts:75 | {id, name, baseUrl, auth, getModels(), stream(), streamSimple()} |
| Models | packages/ai/src/models.ts:127 | Provider 容器 + auth 解析 + 统一 stream/streamSimple/complete/completeSimple |
| ModelsImpl.streamSimple | packages/ai/src/models.ts:512 | lazyStream → requireProvider → applyAuth → provider.streamSimple() |
lazyStream:按需加载 SDK
packages/ai/src/api/lazy.ts:46 的 lazyStream 是 Pi 性能优化的核心:
// 同步返回 stream,异步在背后跑 setup
export function lazyStream<T>(
setup: () => Promise<StreamFunction>,
): StreamFunction {
return async function*(model, context, options) {
const real = await setup() // 动态 import() SDK
yield* real(model, context, options)
}
}
lazyApi()(lazy.ts:68)包装动态 import()——Provider 实现按需加载。主 bundle 不含任何 SDK,anthropic-messages.ts 高达 1351 行(含 @anthropic-ai/sdk 调用、cache_control、thinking),但只有用到时才加载。
pi-ai 是独立实现,不依赖也不兼容 @ai-sdk/*。vercel-ai-gateway 只是一个普通 Provider——通过 OpenAICompletionsCompat.vercelGatewayRouting 字段配置路由。自己写多 Provider 适配的代价是 工程复杂度,收益是 完全控制(消息/工具/事件协议自己定义)和 零外部依赖(每个 Provider SDK 可独立升级)。
packages/ai/src/api/lazy.ts:46-60:同步返回 outer stream,setup(auth + 动态 import)在背后跑。失败变成流上的 error 事件。
三个最独特设计
jiti 动态加载 TS 扩展,30+ 事件钩子(tool_call / before_provider_request / session_before_compact…) + registerTool/Command/Shortcut/Flag。Agent 可以在运行时给自己加工具、加命令、加快捷键。pi-protocol 用 CBOR + 长度帧 + TypeBox 校验 + token 鉴权把 server 和 client 解耦。多客户端可以 attach 到同一个 session,server 做快照广播。不是 JSON-RPC,也不是 MCP——自定义的会话级二进制协议。pi-tui 无 React、无 Virtual DOM、无 flexbox——组件就是 render(width): string[],渲染靠逐行字符串 diff只刷变化行,配 \x1b[?2026h/l synchronized output。比 Ink(Claude Code 用的)I/O 更少、闪烁更小。代价与收益
| 设计 | 代价 | 收益 |
|---|---|---|
| 自扩展 Extensions | 运行时动态加载有安全风险(恶意 TS) | Agent 可以改变自己的能力面(不是"插件"那种预定义扩展点) |
| 协议驱动进程分离 | 工程复杂度高(9 个核心包的依赖管理、版本同步) | 多客户端 attach 同一 session、可替换存储与传输 |
| 差分渲染 TUI | 布局能力弱(无 flexbox),组件需自己处理换行/截断 | 终端 I/O 极少、对慢终端友好、spinner 动画只刷一行 |
协议驱动
Pi 把 Agent 拆成 server / client / storage / runtime 四层,靠显式二进制协议粘合——这是与 Claude Code 单体的本质区别。
pi-protocol:自定义二进制线协议
pi-protocol 包(12 个 TS 文件)定义了 Pi 的 wire format。不是 JSON-RPC,也不是 MCP——它是自定义的请求/响应 + 事件流协议,管 session 生命周期(create/attach/detach/abort)和 transcript 流式同步。
flowchart LR
subgraph WIRE["线协议 = 长度帧 + CBOR"]
LEN["4 字节大端
长度前缀"]
CBOR["CBOR 编码
payload"]
end
subgraph SCHEMA["TypeBox 校验"]
HELLO["ClientHello / ServerHello
(握手首帧)"]
REQ["RequestEnvelope
(id + command)"]
RES["ResponseEnvelope
(ok/error)"]
EVT["EventEnvelope
(server/session snapshot)"]
end
subgraph CMD["命令"]
C1["list / create / attach"]
C2["detach / prompt / steer"]
C3["abort / set_model / set_thinking"]
end
WIRE --> SCHEMA
SCHEMA --> CMDpi-protocol 的三层结构:wire format → schema → command 集合。PROTOCOL_VERSION = 2,DEFAULT_MAX_FRAME_LENGTH = 16MB。核心数据结构
TranscriptItem(schemas.ts):user / assistant / tool 三类;assistant 有streaming/complete/error/aborted四态。TranscriptProgress(schemas.ts):item_started/assistant_delta/item_updated/item_finished,增量流式。SessionSnapshot(schemas.ts):含revision+transcript+queuedSteer——revision 用于客户端单调丢弃旧快照。
和 MCP 的关系:pi-protocol 是 Pi 自己的会话级协议,MCP 是工具调用协议——二者正交。pi-protocol 传输的
toolCall/tooltranscript 项里才包含工具语义,但协议本身不规定工具发现/调用——那是coding-agent层的事。protocol/src/schemas.ts:381-447
client 测试 enforce exclusive/shared)。
多客户端 attach 同一 session
pi-server(packages/server/src/server.ts,399 行)实现 PiSessionBackend/PiSessionRuntime 接口,runtime 实际由 coding-agent 的 AgentSession 提供。多客户端 attach 同一 session是它最大的设计目的。
| 核心组件 | 位置 | 职责 |
|---|---|---|
| PiServer | server.ts:38-80 | 主类,注入 PiSessionBackend + token(sha256 + timingSafeEqual) |
| LiveSessionManager | sessions.ts:43-353 | 维护 liveSessions Map;openingSessions 防并发打开;maybeDispose 空闲自动卸载 |
| ServerSnapshotPublisher | snapshots.ts | revision 化的快照广播——客户端按 revision 单调递增丢弃旧快照 |
| PiSessionBackend | types.ts:43-53 | 接口契约:createSession / openSession → PiSessionRuntime |
Session 租约模型
pi-client(packages/client/src/client.ts,433 行)实现了精巧的租约模型:
createSession()→ 独占租约(exclusive):只有一个连接能操作attachSession()→ 共享租约(shared):多个连接可以同时订阅快照- 引用计数
#sessionLeaseCounts:最后一个 release 时才发detach命令 #sessionLeaseGenerations:detach 失败时标记#sessionCleanupRequired,下次 acquire 前#reconcileSessionCleanup重试
这与 CodePilot 的"宿主+三 Runtime"思路完全相反:CodePilot 把三 Runtime 都装进同一个桌面进程,Pi 把一个 Runtime 跑在独立 server 上、让多种客户端来 attach。
Unix socket 而不是 HTTP 是有意为之——
listener.ts:1-4的PiServerListener只接受 Unix transport,Windows 直接抛错。传输层抽象为ByteConnection(connection.ts),任何能给出有序字节流的传输都可接入。这让 Pi 可以未来扩展到 WebSocket、stdio、TCP——只需替换 transport,协议不变。server/src/transports/unix/listener.ts:22
Agent 核心
pi-agent-core 包把 Agent runtime 抽象成 provider-agnostic 的可配置状态机——LLM 调用是注入点(StreamFn),消息是开放联合类型(AgentMessage + CustomAgentMessages),控制流分支被抽成 4 个回调钩子。
双层 while 主循环
packages/agent/src/agent-loop.ts:155-275 的 runLoop() 是 Pi 的核心——一个双层 while(true) 循环,不是状态机也不是纯事件驱动:
flowchart TB
A["外层 while(true)
处理 follow-up 消息"]
A --> B["内层 while
(hasMoreToolCalls || pendingMessages.length > 0)"]
B --> C["1. streamAssistantResponse()
→ pi-ai Models.streamSimple()
→ AssistantMessageEventStream"]
C --> D{"2. 检查 stopReason"}
D -->|error/aborted| E["立即终止"]
D -->|length| F["截断保护:
failToolCallsFromTruncatedMessage()"]
D -->|tool_use| G["3. executeToolCalls()
parallel: Promise.all"]
G --> H["4. prepareNextTurn()
harness 钩子:
compaction + 刷 system prompt"]
H --> I{"5. shouldStopAfterTurn()?"}
I -->|是| J["退内层"]
I -->|否| B
J --> K["getFollowUpMessages()?"]
K -->|有| A
K -->|无| L["结束"]与 Claude Code 的本质区别
| 维度 | Claude Code | pi-agent-core |
|---|---|---|
| 循环结构 | 单层 while + 显式 stop_reason 分支 | 双层 while:内层 tool-call + 外层 follow-up;4 个钩子参数化 |
| 流式接入 | 直接调 Anthropic SDK | 通过 StreamFn 注入 + lazyStream 异步 setup |
| 工具执行 | 串行 | 默认 parallel(Promise.all);per-tool executionMode 可覆盖 |
| 消息模型 | provider 原生 message 格式 | AgentMessage = LLM Message ∪ CustomAgentMessages(declaration merging) |
| 可插拔性 | 工具 + 权限系统内建 | 工具是 AgentTool 接口;streamFn/transformContext/convertToLlm 全可注入 |
pi-agent-core 把循环抽象成 provider-agnostic 的可配置状态机——LLM 调用是注入点(
StreamFn),消息是开放联合类型(AgentMessage+CustomAgentMessages),控制流分支被抽成 4 个回调钩子,工具执行模型(并发/串行/拦截/覆盖)完全参数化。代价:抽象层更厚、调试链路更长。
收益:同一套循环能跑 Anthropic/OpenAI/Bedrock 任意 provider,并支持 session 持久化、compaction、steering、follow-up 等 Claude Code 没有原生抽象的能力。
packages/agent/src/agent-loop.ts:170-272。调 tool 轮数 / follow-up / steering,看内外层谁先停。
AgentMessage:开放联合类型
packages/agent/src/types.ts:319 的 AgentMessage = Message | CustomAgentMessages[keyof ...] 是 Pi 与 Claude Code 最大的架构差异之一——通过 declaration merging 扩展自定义消息。
自定义消息类型
harness 注入了以下自定义消息类型(packages/agent/src/harness/messages.ts:54-61):
| 消息类型 | 作用 |
|---|---|
bashExecution | 携带 bash 执行时长、exit code、累积输出,UI 可显示进度 |
custom | 通用自定义,扩展点 |
branchSummary | 分支摘要(fork 后的路径标记) |
compactionSummary | 压缩摘要(被压缩消息的元数据) |
convertToLlm:边界投影
packages/agent/src/harness/messages.ts:120 的 convertToLlm 函数是关键——它把这些自定义消息投影成 LLM 可懂的 user 消息或过滤掉。
这个设计的精髓:UI/存储能用富消息(带元数据、可交互、可分支),而 LLM 只看标准
user/assistant/tool消息。中间通过convertToLlm做边界投影——不把内部表示泄漏给模型,也不让模型被迫理解私有协议。packages/agent/src/harness/messages.ts:120
三层状态管理
pi-agent-core 不是单层状态——它把状态分成三个抽象层级,每层关注不同的事:
flowchart TB
subgraph L1["① AgentContext(types.ts:406)"]
C1["纯快照
{systemPrompt, messages, tools}
每个 turn 都 snapshot"]
end
subgraph L2["② Agent 类(agent.ts:171)"]
C2["MutableAgentState
系统提示 / 模型 / thinking
工具 / 消息 / isStreaming
tools & messages 用 getter/setter
拷贝写入(防外部 mutate)"]
end
subgraph L3["③ AgentHarness(agent-harness.ts:173)"]
C3["在 Agent 之上叠加:
Session 持久化
compaction
skills 注入
prompt templates
provider hooks"]
end
L1 --> L2
L2 --> L3每个层级的取舍
subscribe() 监听事件,steer()/followUp() 走 PendingMessageQueue("all" 或 "one-at-a-time" 两种 drain 模式)。prepareNextTurn 钩子(L527)每个 turn 重建 turn state——重新 session.buildContext() + 刷新 system prompt。Provider hooks(before_provider_request/tool_call/tool_result/context)。prepareNextTurn / shouldStopAfterTurn / getSteeringMessages / getFollowUpMessages——控制流分支被抽成回调,不是硬编码。pi-agent-core 是三层:
AgentContext 快照 → Agent 有状态封装 → AgentHarness 持久化与扩展。代价是抽象更厚;收益是任何一层都可以独立测试、独立替换、独立扩展。
自扩展生态
Pi 把"扩展"做到极致:Agent 运行时给自己加工具、加命令、加快捷键。这不是"插件"(预定义扩展点),是"Agent 改变自己的能力面"。
Extensions 与 jiti 动态加载
packages/coding-agent/src/core/extensions/ 是 Pi 最具差异化的子系统(loader 713 行 / runner 1236 行 / types 1713 行)。
| 关键设计 | 位置 | 作用 |
|---|---|---|
jiti 动态加载 | loader.ts | 不预编译——直接 import TS 源文件。Bun 编译的二进制也支持(VIRTUAL_MODULES) |
VIRTUAL_MODULES | loader.ts:50 | 预注入 pi-agent-core / pi-ai / pi-tui / typebox,让扩展无需 npm install |
ExtensionAPI | types.ts:1193 | 暴露 30+ 事件钩子(tool_call/before_provider_request/session_before_compact…) |
| register* | types.ts | registerTool / registerCommand / registerShortcut / registerFlag |
一个扩展长什么样
.pi/extensions/tps.ts(仓库内置的示例)就是一段普通 TS——它通过 jiti 被 loader.ts 动态 require,然后调用 ExtensionAPI.registerTool 注册一个 "tps" 工具。不需要打包、不需要 npm install、不需要重启——resource-loader.ts:reload() 会热重载。
Pi 的自扩展是 "Agent 可以改变自己的能力面"——不只是工具,还包括:
•
registerCommand:加斜杠命令•
registerShortcut:加快捷键•
registerFlag:加 CLI flag• 事件钩子:
tool_call/before_provider_request/session_before_compact…Agent 可以写一个扩展,给模型加 4 个新工具、改写 6 个钩子、注册 3 个新命令——一次 reload 全部生效。
Skills · Prompt Templates · Themes
.pi/ 目录除了 extensions/ 还有三个子目录——它们一起构成 Pi 的内容层。
| 目录 | 发现路径 | 作用 |
|---|---|---|
skills/ | ~/.pi/agent/skills / <cwd>/.pi/skills / --skills | 遵循 agentskills.io 规范;SKILL.md + frontmatter;<available_skills> XML 注入 system prompt |
prompts/ | 同上 + --prompt | Prompt templates;.md 文件(cl.md / is.md / pr.md / sa.md / wr.md) |
themes/ | ~/.pi/agent/themes / <cwd>/.pi/themes | TUI 颜色主题 |
AGENTS.md / CLAUDE.md | resource-loader.ts:71 | 上下文文件(项目级) |
Skills 加载机制
packages/coding-agent/src/core/skills.ts(487 行)实现完整流程:
- 发现:
loadSkillsFromDir(L168)递归扫描,遇到SKILL.md即视为 skill root 不再下钻 - 解析:
loadSkillFromFile(L277)用parseFrontmatter提取 name / description /disable-model-invocation - 冲突解决:user → project → path 优先级;同名产生
collision诊断 - 注入:
formatSkillsForPrompt(L335)生成<available_skills><skill>...</skill></available_skills>XML 块,指示模型用 read 工具加载 skill 文件
Skills 不是"自动调用"——是"告诉模型有哪些 skill 可用,让模型自己选择要不要 read"。
disable-model-invocation=true的 skill 不进提示,只能/skill:name显式调用。这种"模型自决"的注入方式比"自动注入"更安全——模型可以根据上下文判断是否需要某个 skill。packages/coding-agent/src/core/skills.ts:99-105
TUI 与持久化
Pi 自研差分渲染 TUI(无 React);CLI 默认用 JSONL SessionManager,另提供 SQLite 事件溯源 SessionStore——这两个选择都和"工程复杂度换极致体验"有关。
pi-tui:差分渲染
packages/tui/(74 个 TS 文件)是 Pi 自研的终端 UI 库。无 React、无 Virtual DOM、无 flexbox——组件就是 render(width): string[],渲染靠逐行字符串 diff。
| 对比维度 | Ink(Claude Code) | pi-tui |
|---|---|---|
| 框架 | React + Yoga | 无框架 |
| 布局 | flexbox | 手写 layout.ts:renderLayoutFrame |
| 渲染 | 全量重绘(每帧 reconcile→render 整棵树) | 逐行字符串 diff只刷变化行 |
| 抽象层 | 厚(reconciler + component tree) | 薄(组件即 render(): string[]) |
| I/O | 多(全量输出) | 少(只写变化行) |
| 闪烁 | 有(需 double buffering) | 无(\x1b[?2026h/l synchronized output) |
差分渲染算法
TuiMainScreen.doRender()(TuiMainScreen.ts:146)的核心算法:
render(width)渲染全部组件为字符串行数组- 合成 overlays(
compositeOverlays) - 提取
CURSOR_MARKER(APC 序列\x1b_pi:c\x07)定位硬件光标用于 IME - 逐行 diff(L261-274):遍历
previousLinesvsnewLines,找firstChanged/lastChanged - 特殊路径:首帧 / 宽度变化 / 高度变化 / 内容缩过多 →
fullRender(true)全清 - 增量写(L356-495):用
\x1b[?2026h…\x1b[?2026l包裹,computeLineDiff算光标位移,只对firstChanged..lastChanged区间行写\x1b[2K+ 新内容 - Kitty 图片特殊处理:
expandChangedRangeForKittyImages把图片占用的多行纳入 diff 范围
utils.ts 的 visibleWidth/sliceByColumn/wrapTextWithAnsi);布局能力弱(无 flexbox);整个 TUI 系统约 5000 行,比 Ink 生态自己写差得远。但收益是 终端 I/O 极少——spinner 动画只刷一行,模型输出滚动只写变化的几行。在 SSH/慢终端上差别巨大。
键盘处理
packages/tui/src/keys.ts(1401 行)同时支持:
- 传统转义序列(
\x1b[A= Up) - Kitty keyboard protocol(
setKittyProtocolActive/isKeyRelease/isKeyRepeat)——区分按下/释放/重复
stdin-buffer.ts(434 行)把 stdin chunk 按 ESC 序列边界拆分成原子事件,避免半个 CSI 被分发。packages/tui/src/stdin-buffer.ts:1
TuiMainScreen 的 firstChanged/lastChanged:只重绘变化行。改「下一帧」,看写了多少字节。
SQLite 事件溯源
packages/storage/sqlite-node/(12 个 TS 文件)实现 session 持久化。用 Node 22+ 内置的 node:sqlite,无第三方原生依赖。
六张表
-- 001_initial.sql
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
cwd TEXT NOT NULL,
parent_session_id TEXT,
active_leaf_id TEXT,
metadata TEXT
);
CREATE TABLE session_entries (
session_id TEXT NOT NULL,
id TEXT NOT NULL,
entry_seq INTEGER NOT NULL,
parent_id TEXT,
type TEXT NOT NULL, -- message / thinking_level_change / model_change /
-- active_tools_change / compaction / branch_summary /
-- custom / custom_message / label / session_info / leaf
timestamp INTEGER NOT NULL,
payload TEXT NOT NULL, -- JSON
PRIMARY KEY (session_id, id)
);
CREATE TABLE session_sequences (
session_id TEXT PRIMARY KEY,
next_seq INTEGER NOT NULL
);
CREATE TABLE branch_entries (
branch_id TEXT,
entry_id TEXT,
seq INTEGER,
PRIMARY KEY (branch_id, entry_id)
);
CREATE TABLE session_materialized (
session_id TEXT PRIMARY KEY,
snapshot TEXT -- 整体摘要
);
CREATE TABLE entry_materialized (
session_id TEXT,
entry_id TEXT,
type TEXT,
payload TEXT,
PRIMARY KEY (session_id, entry_id)
);
SessionManager 写 JSONL(与 Claude Code 同为文件树;/fork /tree /compact 都跑在这条路上)。可选:
pi-storage-sqlite-node 实现 harness 的 SessionStore(事件溯源 + 物化视图)——不被默认交互 CLI 直接使用。读本章时不要把 SQLite 当成「Pi 日常默认持久化」。
并发控制:
SerialOperationQueue(session-store.ts:45-60)串行化所有写,SqliteSessionConnection每 session 缓存一个 writer 连接。PRAGMA journal_mode=WAL; synchronous=FULL; busy_timeout=5000(session-store.ts:33-37)。packages/storage/sqlite-node/src/sqlite/session-store.ts:33-60
分支 · fork · compaction
Pi 的 storage 不是简单的 CRUD——它把分支、fork、compaction 都做成 session 树操作。
leaf entry 触发 materializeBranch() 重算 branch_entries。一个 session 可以有多个 leaf(多个分支),用 active_leaf_id 标记当前。fork() 在事务内 readSessionEntriesForFork(source, selection) 复制选区到新 session。可以在 fork 命令行指定源 session 选区。compaction entry 支持 retainedTail(保留尾部)+ firstKeptEntryId(压缩后保留的第一条)。压缩 + 保留尾部原子完成。appendEntry() 时同时更新 session_materialized(整体摘要)和 entry_materialized(按 type 分行)。读时直接查物化表,不重放整树。• 用户可能回滚到任意 checkpoint
• 用户可能分支尝试不同方向
• 用户可能压缩但保留关键决策
• 用户可能fork从一个 session 派生出新的
Pi 的 CLI 已在 JSONL SessionManager 上实现 fork/tree/compact;SQLite SessionStore 是另一条可选后端,用事件溯源 + 物化视图把同类操作收成单条 entry 写入。二者不要互相否定。
retainedTail + firstKeptEntryId)。拖预算和尾部长度,看摘要空间还剩多少。
架构对照
Pi 在十七家之外的位置:与 Claude Code / CodePilot / open-design 三家最相关的项目做逐项对照。
与 Claude Code · CodePilot · open-design
与 Claude Code 的对比(最直接的对标)
| 维度 | Claude Code | Pi | 差异本质 |
|---|---|---|---|
| 架构 | 单体 CLI | monorepo 9 包 | 一体化 vs 可组合 |
| 主循环 | 单层 while | 双层 while | 命令式 vs 参数化 |
| Provider | 锁 Anthropic | 38 个 | 封闭 vs 开放 |
| TUI | Ink(React + Yoga) | 差分渲染 | 保留模式 vs 立即模式 |
| 权限 | 五关裁决 | 不内建 | 应用级 vs OS 级 |
| 扩展 | 插件 + MCP | Extensions + Skills(MCP 外置) | 有限 vs 自扩展 |
| 持久化 | JSONL | CLI JSONL + 可选 SQLite SessionStore | 默认同为文件树 |
| 会话 | 单进程 | 多 session + attach/detach | 单一 vs 多客户端 |
| 模型 | MIT | MIT | 相同 |
与 CodePilot 的对比(同为多 Provider)
| 维度 | CodePilot | Pi | 差异本质 |
|---|---|---|---|
| 界面 | Electron 桌面 GUI | 终端 TUI | 桌面 vs 终端 |
| Provider | 17+ | 38 | 少 vs 多 |
| Runtime | 三条可替换 | 一条(agent-core) | 多引擎 vs 单引擎 |
| 协议 | HTTP/SSE | 二进制 CBOR | Web vs Unix |
| 扩展 | MCP + Skills | Extensions + Skills(MCP 外置) | 标准 vs 自扩展 |
| 持久化 | SQLite(CRUD) | CLI JSONL;harness 可选 SQLite | 产品默认不同 |
| 权限 | 三层 | 不内建 | 有 vs 无 |
| 会话 | 单会话 + rewind | 多 session + fork | 回滚 vs 分支 |
与 open-design 的对比(同为"宿主"形态)
| 维度 | open-design | Pi | 差异本质 |
|---|---|---|---|
| 宿主对象 | 25 个 CLI | 38 个 Provider | 引擎数量 vs Provider 数量 |
| 核心创新 | 适配器即数据 | 自扩展 Extensions | 数据规格 vs 代码扩展 |
| 主循环 | 不写(外包) | 双层 while(自研) | 纯宿主 vs 自主+SDK |
| 协议 | HTTP/SSE | 二进制 CBOR | Web vs Unix |
设计哲学与取舍
Pi 的核心取舍
收益:多客户端 attach 同一 session、可替换存储与传输、每层独立测试。
收益:权限系统太复杂(Claude Code 五关裁决),容器化更安全(OS 级隔离)。
收益:终端 I/O 极少、慢终端无闪烁、spinner 只刷一行。
node:sqlite、需要 schema、需要迁移。收益:分支/fork/compaction/rewind 都是单条 entry 写入,物化视图自动更新。
Pi 的独特贡献
② 协议驱动进程分离:把 Agent 拆成 server/client/storage/runtime 四层,靠显式二进制协议粘合。这让多客户端 attach 同一 session成为可能。
③ 差分渲染 TUI:无 React、无 Virtual DOM,逐行 diff 只刷变化行。在慢终端上闪烁极小,I/O 极少。
给阅读者的建议
- 想理解 Agent runtime 的抽象:先读
packages/agent/src/agent-loop.ts的双层 while(ch6)——这是 Pi 的"心脏" - 想理解多 Provider 怎么统一:先读
packages/ai/src/api/lazy.ts的lazyStream(ch2)——按需加载是核心 - 想理解协议设计:先读
packages/protocol/src/schemas.ts(ch4)——TypeBox 校验是契约的形态 - 想理解 TUI 怎么做:先读
packages/tui/src/TuiMainScreen.ts的doRender()(ch11)——逐行 diff 是关键 - 想理解自扩展:先读
packages/coding-agent/src/core/extensions/loader.ts(ch9)——jiti 是机制 - 想理解持久化:先读
packages/storage/sqlite-node/src/sqlite/session-store.ts(ch12-13)——事件溯源 + 物化视图
Pi 不是 Claude Code 的复刻,也不是另一个"轻量级 Claude Code"。它是 Agent 工程的另一种可能性:把协议、运行时、UI、存储拆开,靠显式契约粘合。工程复杂度极高,但换来的是可组合性、可替换性、可观测性。