源码解析 第十七份 main 分支

自扩展 Agent
的 monorepo

Pi 不是又一个终端编程 Agent——它是把 Agent 拆成 9 个正交 npm 包的"协议驱动可组合 Agent 基础设施"。 同一个 Agent runtime,38 个 Provider差分渲染 TUI(无 React)、二进制线协议(CBOR)、 CLI 默认 JSONL(另有 SQLite SessionStore)、运行时自扩展——靠显式契约而非进程边界粘合。含 5 个可交互实验台

9
npm 包(monorepo)
38
Provider(lazyStream)
30+
扩展钩子(jiti 动态加载)
2
while(双层主循环)
5
可交互实验台
Part I

它是什么

先把身世说清楚:这是一个 monorepo。读 monorepo 的正确方式是读层次——9 个 npm 包彼此正交,靠显式契约粘合。

Chapter 01

9 包 monorepo 的解剖

打开仓库第一眼就该注意到的事:根目录有 9 个 packages/ 子目录,彼此通过 "workspace:*" 互相依赖。Pi 不是把 Agent 塞进一个进程——它把 Agent 拆成 9 个独立 npm 包(其中 packages/storage/ 本身不是包,真正的存储包在 packages/storage/sqlite-node/),每个可以单独发布、单独嵌入。

运行时层
agent-core + ai
pi-agent-core(62 文件)+ pi-ai(305 文件)—— 纯 SDK,无 UI、无 CLI、可被任何宿主嵌入。
组装层
coding-agent + tui
pi-coding-agent(488 文件)+ pi-tui(74 文件)—— 终端 CLI + 差分渲染 UI
协议层
protocol + server + client
pi-protocol(12 文件)+ pi-server(38 文件)+ pi-client(18 文件)—— CBOR 二进制线协议,让进程解耦。
基础设施层
storage + evals
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.tsAgentSession(coding-agent 与 agent-core 的桥梁)
1 223 行packages/tui/src/tui.tsTUI 基类(差分渲染引擎)
1 401 行packages/tui/src/keys.ts键盘处理(传统转义序列 + Kitty 协议)
487 行packages/coding-agent/src/core/skills.tsSkills 系统(agentskills.io 规范)
713 行packages/coding-agent/src/core/extensions/loader.tsExtensions 动态加载器(jiti)

读完这份报告,你会看到 三个在前面所有 Agent 项目里都没有的设计:自扩展 Extensions协议驱动进程分离差分渲染 TUI。这三个加起来,让 Pi 成为"Agent 工程的另一种可能性"。

Chapter 02

38 Provider 的统一接口

Pi 支持 38 个 Provider(OpenAI/Anthropic/Google/Bedrock/DeepSeek/Groq/Kimi/Moonshot/ZAI/Xiaomi…),但不依赖 Vercel AI SDK。它的 pi-ai 包是完全独立实现——每个 Provider 的 SDK 适配自己写,主 bundle 不含任何 SDK。

抽象层级位置做什么
KnownApipackages/ai/src/types.ts:16-2610 个 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()}
Modelspackages/ai/src/models.ts:127Provider 容器 + auth 解析 + 统一 stream/streamSimple/complete/completeSimple
ModelsImpl.streamSimplepackages/ai/src/models.ts:512lazyStream → requireProvider → applyAuth → provider.streamSimple()

lazyStream:按需加载 SDK

packages/ai/src/api/lazy.ts:46lazyStream 是 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),但只有用到时才加载。

为什么不依赖 Vercel AI SDK pi-ai 是独立实现,不依赖也不兼容 @ai-sdk/*vercel-ai-gateway 只是一个普通 Provider——通过 OpenAICompletionsCompat.vercelGatewayRouting 字段配置路由。

自己写多 Provider 适配的代价是 工程复杂度,收益是 完全控制(消息/工具/事件协议自己定义)和 零外部依赖(每个 Provider SDK 可独立升级)。
LAB 03 lazyStream 时序 packages/ai/src/api/lazy.ts:46-60同步返回 outer stream,setup(auth + 动态 import)在背后跑。失败变成流上的 error 事件。
setup 延迟 ms
Chapter 03

三个最独特设计

设计一
自扩展 Extensions
jiti 动态加载 TS 扩展,30+ 事件钩子tool_call / before_provider_request / session_before_compact…) + registerTool/Command/Shortcut/Flag。Agent 可以在运行时给自己加工具、加命令、加快捷键。
设计二
协议驱动进程分离
pi-protocolCBOR + 长度帧 + TypeBox 校验 + token 鉴权把 server 和 client 解耦。多客户端可以 attach 到同一个 session,server 做快照广播。不是 JSON-RPC,也不是 MCP——自定义的会话级二进制协议。
设计三
差分渲染 TUI
pi-tui 无 React、无 Virtual DOM、无 flexbox——组件就是 render(width): string[],渲染靠逐行字符串 diff只刷变化行,配 \x1b[?2026h/l synchronized output。比 Ink(Claude Code 用的)I/O 更少、闪烁更小。
三个设计的共同点 它们都是"把复杂性往下沉":自扩展把扩展点下沉到运行时、协议把进程边界下沉到 wire format、TUI 把重绘下沉到 dirty-row。共同点是 不追求"统一抽象",而是让每一层做自己最擅长的事。

代价与收益

设计代价收益
自扩展 Extensions运行时动态加载有安全风险(恶意 TS)Agent 可以改变自己的能力面(不是"插件"那种预定义扩展点)
协议驱动进程分离工程复杂度高(9 个核心包的依赖管理、版本同步)多客户端 attach 同一 session、可替换存储与传输
差分渲染 TUI布局能力弱(无 flexbox),组件需自己处理换行/截断终端 I/O 极少、对慢终端友好、spinner 动画只刷一行
Part II

协议驱动

Pi 把 Agent 拆成 server / client / storage / runtime 四层,靠显式二进制协议粘合——这是与 Claude Code 单体的本质区别。

Chapter 04

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 --> CMD
图 1 pi-protocol 的三层结构:wire format → schema → command 集合。PROTOCOL_VERSION = 2DEFAULT_MAX_FRAME_LENGTH = 16MB

核心数据结构

  • TranscriptItemschemas.ts):user / assistant / tool 三类;assistant 有 streaming / complete / error / aborted 四态。
  • TranscriptProgressschemas.ts):item_started / assistant_delta / item_updated / item_finished,增量流式。
  • SessionSnapshotschemas.ts):含 revision + transcript + queuedSteer——revision 用于客户端单调丢弃旧快照。

和 MCP 的关系:pi-protocol 是 Pi 自己的会话级协议,MCP 是工具调用协议——二者正交。pi-protocol 传输的 toolCall/tool transcript 项里才包含工具语义,但协议本身不规定工具发现/调用——那是 coding-agent 层的事。protocol/src/schemas.ts:381-447

LAB 02 租约:shared vs exclusive 多客户端 attach 同一 session:shared 可叠加;exclusive 与任何租约互斥(见 client 测试 enforce exclusive/shared)。
活跃租约
Chapter 05

多客户端 attach 同一 session

pi-serverpackages/server/src/server.ts,399 行)实现 PiSessionBackend/PiSessionRuntime 接口,runtime 实际由 coding-agentAgentSession 提供。多客户端 attach 同一 session是它最大的设计目的。

核心组件位置职责
PiServerserver.ts:38-80主类,注入 PiSessionBackend + token(sha256 + timingSafeEqual
LiveSessionManagersessions.ts:43-353维护 liveSessions Map;openingSessions 防并发打开;maybeDispose 空闲自动卸载
ServerSnapshotPublishersnapshots.tsrevision 化的快照广播——客户端按 revision 单调递增丢弃旧快照
PiSessionBackendtypes.ts:43-53接口契约:createSession / openSession → PiSessionRuntime

Session 租约模型

pi-clientpackages/client/src/client.ts,433 行)实现了精巧的租约模型:

  • createSession()独占租约(exclusive):只有一个连接能操作
  • attachSession()共享租约(shared):多个连接可以同时订阅快照
  • 引用计数 #sessionLeaseCounts:最后一个 release 时才发 detach 命令
  • #sessionLeaseGenerations:detach 失败时标记 #sessionCleanupRequired,下次 acquire 前 #reconcileSessionCleanup 重试
"进程分离"的工程含义 Pi 不是"为了分布而分布"——它把 Agent runtime 放进 server、把 UI/客户端放进 client,目的是让 runtime 可以被多个客户端共享。一个用户在桌面 TUI 操作,另一用户在手机上通过 IDE 看进度——同一个 session,两种客户端。

这与 CodePilot 的"宿主+三 Runtime"思路完全相反:CodePilot 把三 Runtime 都装进同一个桌面进程,Pi 把一个 Runtime 跑在独立 server 上、让多种客户端来 attach。

Unix socket 而不是 HTTP 是有意为之——listener.ts:1-4PiServerListener 只接受 Unix transport,Windows 直接抛错。传输层抽象为 ByteConnectionconnection.ts),任何能给出有序字节流的传输都可接入。这让 Pi 可以未来扩展到 WebSocket、stdio、TCP——只需替换 transport,协议不变。server/src/transports/unix/listener.ts:22

Part III

Agent 核心

pi-agent-core 包把 Agent runtime 抽象成 provider-agnostic 的可配置状态机——LLM 调用是注入点(StreamFn),消息是开放联合类型(AgentMessage + CustomAgentMessages),控制流分支被抽成 4 个回调钩子。

Chapter 06

双层 while 主循环

packages/agent/src/agent-loop.ts:155-275runLoop() 是 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["结束"]
图 2 双层 while:内层管"模型→工具→回灌",外层管"agent 自己可停但用户还想继续"。比 Claude Code 的单层 while 灵活一个维度。

与 Claude Code 的本质区别

维度Claude Codepi-agent-core
循环结构单层 while + 显式 stop_reason 分支双层 while:内层 tool-call + 外层 follow-up;4 个钩子参数化
流式接入直接调 Anthropic SDK通过 StreamFn 注入 + lazyStream 异步 setup
工具执行串行默认 parallelPromise.all);per-tool executionMode 可覆盖
消息模型provider 原生 message 格式AgentMessage = LLM Message ∪ CustomAgentMessages(declaration merging)
可插拔性工具 + 权限系统内建工具是 AgentTool 接口;streamFn/transformContext/convertToLlm 全可注入
本质区别 Claude Code 的循环是"为 Anthropic API 量身写的命令式 driver",控制流硬编码、消息格式紧耦合 provider。

pi-agent-core 把循环抽象成 provider-agnostic 的可配置状态机——LLM 调用是注入点(StreamFn),消息是开放联合类型(AgentMessage+CustomAgentMessages),控制流分支被抽成 4 个回调钩子,工具执行模型(并发/串行/拦截/覆盖)完全参数化。

代价:抽象层更厚、调试链路更长。
收益:同一套循环能跑 Anthropic/OpenAI/Bedrock 任意 provider,并支持 session 持久化、compaction、steering、follow-up 等 Claude Code 没有原生抽象的能力。
LAB 01 双层 while 停机协议 移植 packages/agent/src/agent-loop.ts:170-272。调 tool 轮数 / follow-up / steering,看内外层谁先停。
tool 轮数(内层)
follow-up 条数(外层)
stopReason
Chapter 07

AgentMessage:开放联合类型

packages/agent/src/types.ts:319AgentMessage = 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:120convertToLlm 函数是关键——它把这些自定义消息投影成 LLM 可懂的 user 消息或过滤掉

这个设计的精髓:UI/存储能用富消息(带元数据、可交互、可分支),而 LLM 只看标准 user/assistant/tool 消息。中间通过 convertToLlm 做边界投影——不把内部表示泄漏给模型,也不让模型被迫理解私有协议。packages/agent/src/harness/messages.ts:120

Chapter 08

三层状态管理

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
图 3 三层状态:快照 → 有状态封装 → 持久化与扩展。每一层只关注自己那一级的关注点。

每个层级的取舍

AgentContext
不可变快照
每个 turn 都重新 snapshot,让 harness 可以无损替换(compaction 后、注入 skills 后、切换模型后),不必担心外部 mutate。
Agent
事件源 + 队列
subscribe() 监听事件,steer()/followUp()PendingMessageQueue"all""one-at-a-time" 两种 drain 模式)。
AgentHarness
完整生命周期
prepareNextTurn 钩子(L527)每个 turn 重建 turn state——重新 session.buildContext() + 刷新 system prompt。Provider hooks(before_provider_request/tool_call/tool_result/context)。
钩子扩展
4 个回调
prepareNextTurn / shouldStopAfterTurn / getSteeringMessages / getFollowUpMessages——控制流分支被抽成回调,不是硬编码。
与 Claude Code 的对照 Claude Code 是单一 conversation 数组——所有状态都在一个 messages 列表里。

pi-agent-core 是三层AgentContext 快照 → Agent 有状态封装 → AgentHarness 持久化与扩展。

代价是抽象更厚;收益是任何一层都可以独立测试、独立替换、独立扩展。
Part IV

自扩展生态

Pi 把"扩展"做到极致:Agent 运行时给自己加工具、加命令、加快捷键。这不是"插件"(预定义扩展点),是"Agent 改变自己的能力面"。

Chapter 09

Extensions 与 jiti 动态加载

packages/coding-agent/src/core/extensions/ 是 Pi 最具差异化的子系统(loader 713 行 / runner 1236 行 / types 1713 行)。

关键设计位置作用
jiti 动态加载loader.ts不预编译——直接 import TS 源文件。Bun 编译的二进制也支持(VIRTUAL_MODULES
VIRTUAL_MODULESloader.ts:50预注入 pi-agent-core / pi-ai / pi-tui / typebox,让扩展无需 npm install
ExtensionAPItypes.ts:1193暴露 30+ 事件钩子tool_call/before_provider_request/session_before_compact…)
register*types.tsregisterTool / registerCommand / registerShortcut / registerFlag

一个扩展长什么样

.pi/extensions/tps.ts(仓库内置的示例)就是一段普通 TS——它通过 jitiloader.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 全部生效。
Chapter 10

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/同上 + --promptPrompt templates;.md 文件(cl.md / is.md / pr.md / sa.md / wr.md)
themes/~/.pi/agent/themes / <cwd>/.pi/themesTUI 颜色主题
AGENTS.md / CLAUDE.mdresource-loader.ts:71上下文文件(项目级)

Skills 加载机制

packages/coding-agent/src/core/skills.ts(487 行)实现完整流程:

  1. 发现loadSkillsFromDir(L168)递归扫描,遇到 SKILL.md 即视为 skill root 不再下钻
  2. 解析loadSkillFromFile(L277)用 parseFrontmatter 提取 name / description / disable-model-invocation
  3. 冲突解决:user → project → path 优先级;同名产生 collision 诊断
  4. 注入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

Part V

TUI 与持久化

Pi 自研差分渲染 TUI(无 React);CLI 默认用 JSONL SessionManager,另提供 SQLite 事件溯源 SessionStore——这两个选择都和"工程复杂度换极致体验"有关。

Chapter 11

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)的核心算法:

  1. render(width) 渲染全部组件为字符串行数组
  2. 合成 overlays(compositeOverlays
  3. 提取 CURSOR_MARKER(APC 序列 \x1b_pi:c\x07)定位硬件光标用于 IME
  4. 逐行 diff(L261-274):遍历 previousLines vs newLines,找 firstChanged/lastChanged
  5. 特殊路径:首帧 / 宽度变化 / 高度变化 / 内容缩过多 → fullRender(true) 全清
  6. 增量写(L356-495):用 \x1b[?2026h…\x1b[?2026l 包裹,computeLineDiff 算光标位移,只对 firstChanged..lastChanged 区间行写 \x1b[2K + 新内容
  7. Kitty 图片特殊处理:expandChangedRangeForKittyImages 把图片占用的多行纳入 diff 范围
代价 组件需自己处理换行/截断(utils.tsvisibleWidth/sliceByColumn/wrapTextWithAnsi);布局能力弱(无 flexbox);整个 TUI 系统约 5000 行,比 Ink 生态自己写差得远。

但收益是 终端 I/O 极少——spinner 动画只刷一行,模型输出滚动只写变化的几行。在 SSH/慢终端上差别巨大。

键盘处理

packages/tui/src/keys.ts(1401 行)同时支持:

  • 传统转义序列\x1b[A = Up)
  • Kitty keyboard protocolsetKittyProtocolActive / isKeyRelease / isKeyRepeat)——区分按下/释放/重复

stdin-buffer.ts(434 行)把 stdin chunk 按 ESC 序列边界拆分成原子事件,避免半个 CSI 被分发packages/tui/src/stdin-buffer.ts:1

LAB 04 TUI 逐行 diff 对应 TuiMainScreen 的 firstChanged/lastChanged:只重绘变化行。改「下一帧」,看写了多少字节。
previousLines
newLines
Chapter 12

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)
);
两条持久化路径 CLI 默认:coding-agent 的 SessionManagerJSONL(与 Claude Code 同为文件树;/fork /tree /compact 都跑在这条路上)。

可选pi-storage-sqlite-node 实现 harness 的 SessionStore(事件溯源 + 物化视图)——不被默认交互 CLI 直接使用。

读本章时不要把 SQLite 当成「Pi 日常默认持久化」。

并发控制SerialOperationQueuesession-store.ts:45-60)串行化所有写,SqliteSessionConnection 每 session 缓存一个 writer 连接。PRAGMA journal_mode=WAL; synchronous=FULL; busy_timeout=5000session-store.ts:33-37)。packages/storage/sqlite-node/src/sqlite/session-store.ts:33-60

Chapter 13

分支 · fork · compaction

Pi 的 storage 不是简单的 CRUD——它把分支、fork、compaction 都做成 session 树操作

分支
leaf entry + branch_entries
leaf entry 触发 materializeBranch() 重算 branch_entries。一个 session 可以有多个 leaf(多个分支),用 active_leaf_id 标记当前。
fork
跨 session 复制
fork() 在事务内 readSessionEntriesForFork(source, selection) 复制选区到新 session。可以在 fork 命令行指定源 session 选区
compaction
retainedTail + firstKeptEntryId
compaction entry 支持 retainedTail(保留尾部)+ firstKeptEntryId(压缩后保留的第一条)。压缩 + 保留尾部原子完成
物化视图
避免读时重放
appendEntry() 时同时更新 session_materialized(整体摘要)和 entry_materialized(按 type 分行)。读时直接查物化表,不重放整树
为什么 session 树 + 物化视图重要 LLM Agent 的 session 比传统 CRUD 复杂得多:
• 用户可能回滚到任意 checkpoint
• 用户可能分支尝试不同方向
• 用户可能压缩但保留关键决策
• 用户可能fork从一个 session 派生出新的

Pi 的 CLI 已在 JSONL SessionManager 上实现 fork/tree/compact;SQLite SessionStore 是另一条可选后端,用事件溯源 + 物化视图把同类操作收成单条 entry 写入。二者不要互相否定。
LAB 05 compaction · retainedTail 压缩时尾部条目原样保留(retainedTail + firstKeptEntryId)。拖预算和尾部长度,看摘要空间还剩多少。
context budget
retainedTail 条数
Part VI

架构对照

Pi 在十七家之外的位置:与 Claude Code / CodePilot / open-design 三家最相关的项目做逐项对照。

Chapter 14

与 Claude Code · CodePilot · open-design

与 Claude Code 的对比(最直接的对标)

维度Claude CodePi差异本质
架构单体 CLImonorepo 9 包一体化 vs 可组合
主循环单层 while双层 while命令式 vs 参数化
Provider锁 Anthropic38 个封闭 vs 开放
TUIInk(React + Yoga)差分渲染保留模式 vs 立即模式
权限五关裁决不内建应用级 vs OS 级
扩展插件 + MCPExtensions + Skills(MCP 外置)有限 vs 自扩展
持久化JSONLCLI JSONL + 可选 SQLite SessionStore默认同为文件树
会话单进程多 session + attach/detach单一 vs 多客户端
模型MITMIT相同

与 CodePilot 的对比(同为多 Provider)

维度CodePilotPi差异本质
界面Electron 桌面 GUI终端 TUI桌面 vs 终端
Provider17+38少 vs 多
Runtime三条可替换一条(agent-core)多引擎 vs 单引擎
协议HTTP/SSE二进制 CBORWeb vs Unix
扩展MCP + SkillsExtensions + Skills(MCP 外置)标准 vs 自扩展
持久化SQLite(CRUD)CLI JSONL;harness 可选 SQLite产品默认不同
权限三层不内建有 vs 无
会话单会话 + rewind多 session + fork回滚 vs 分支

与 open-design 的对比(同为"宿主"形态)

维度open-designPi差异本质
宿主对象25 个 CLI38 个 Provider引擎数量 vs Provider 数量
核心创新适配器即数据自扩展 Extensions数据规格 vs 代码扩展
主循环不写(外包)双层 while(自研)纯宿主 vs 自主+SDK
协议HTTP/SSE二进制 CBORWeb vs Unix
Chapter 15

设计哲学与取舍

Pi 的核心取舍

为什么做 monorepo
协议驱动可组合
代价:工程复杂度高(9 个核心包的依赖管理、版本同步、构建顺序)。
收益:多客户端 attach 同一 session、可替换存储与传输、每层独立测试。
为什么不内建权限
下沉到 OS 层
代价:默认运行没有保护,用户必须自己容器化。
收益:权限系统太复杂(Claude Code 五关裁决),容器化更安全(OS 级隔离)。
为什么自研 TUI
差分渲染
代价:布局能力弱、无 flexbox、组件需自己处理换行/截断。
收益:终端 I/O 极少、慢终端无闪烁、spinner 只刷一行。
为什么 SQLite 事件溯源
把 session 做成树
代价:绑定 node:sqlite、需要 schema、需要迁移。
收益:分支/fork/compaction/rewind 都是单条 entry 写入,物化视图自动更新。

Pi 的独特贡献

三个前面十七家都没有的设计 ① 自扩展:Extensions 在运行时给自己加工具/命令/快捷键。这不是"插件"(插件是预定义的扩展点),是"Agent 可以改变自己的能力面"。

② 协议驱动进程分离:把 Agent 拆成 server/client/storage/runtime 四层,靠显式二进制协议粘合。这让多客户端 attach 同一 session成为可能。

③ 差分渲染 TUI:无 React、无 Virtual DOM,逐行 diff 只刷变化行。在慢终端上闪烁极小,I/O 极少。

给阅读者的建议

  1. 想理解 Agent runtime 的抽象:先读 packages/agent/src/agent-loop.ts 的双层 while(ch6)——这是 Pi 的"心脏"
  2. 想理解多 Provider 怎么统一:先读 packages/ai/src/api/lazy.tslazyStream(ch2)——按需加载是核心
  3. 想理解协议设计:先读 packages/protocol/src/schemas.ts(ch4)——TypeBox 校验是契约的形态
  4. 想理解 TUI 怎么做:先读 packages/tui/src/TuiMainScreen.tsdoRender()(ch11)——逐行 diff 是关键
  5. 想理解自扩展:先读 packages/coding-agent/src/core/extensions/loader.ts(ch9)——jiti 是机制
  6. 想理解持久化:先读 packages/storage/sqlite-node/src/sqlite/session-store.ts(ch12-13)——事件溯源 + 物化视图

Pi 不是 Claude Code 的复刻,也不是另一个"轻量级 Claude Code"。它是 Agent 工程的另一种可能性:把协议、运行时、UI、存储拆开,靠显式契约粘合。工程复杂度极高,但换来的是可组合性、可替换性、可观测性。