# Pi 的运行机制与原理

> 从 monorepo 分层、统一 LLM 流式 API、agent 循环，到会话树、压缩与终端差分渲染，逐层解剖 pi 编码智能体的内部实现。

---

LLMS 索引： [llms.txt](/llms.txt)

---

[Pi](https://github.com/earendil-works/pi)（npm 包 `@earendil-works/pi-coding-agent`）自述为「一个极简、可扩展、任你改造的 agent harness」。它刻意不内置子代理、计划模式这类「产品级」功能，而是把一个足够小的内核——模型调用、工具执行、上下文组装、会话存储——做扎实，其余交给扩展、技能、提示词模板和 pi 包生态。

本文基于 monorepo 源码（v1.3.0，2026-10 主分支）逐层分析 pi 的运行机制：一次用户输入如何变成模型请求、工具如何执行、上下文如何在超长会话中存活、终端界面如何高效渲染。阅读不需要预先了解 pi，但需要基本的 LLM agent 概念（system prompt、tool call、上下文窗口）。

文中所有文件路径均相对仓库根目录的 `packages/`。

## 总览：分层与依赖 {#architecture}

下图是交互式架构图（点击节点可查看对应的源码文件与行号证据，可在[新标签页打开](/diagrams/pi-runtime-architecture.html)）：

<figure>
  <iframe src="/diagrams/pi-runtime-architecture.html" title="pi 运行时架构交互图" loading="lazy" style="width:100%;height:900px;border:0;"></iframe>
  <figcaption>pi 运行时架构：一次用户输入自左向右穿过 pi-tui、pi-coding-agent、pi-agent-core、pi-ai 抵达 LLM 提供商；下方为进程内扩展能力，左下为旁路持久化运行时。</figcaption>
</figure>

pi 是一个 npm workspaces monorepo，核心包之间的依赖关系构成一条清晰的分层链：

```
pi-telemetry        供应商中立的遥测契约（span/metrics，显式传递，无全局状态）
   ↑
chord               类型化插件宿主运行时（契约、复制状态、热重载、RPC）
   ↑
pi-ai               统一的多提供商 LLM 流式 API
   ↑
pi-agent-core       通用 agent 循环（状态机 + 工具执行 + 事件流）
   ↑
pi-coding-agent     `pi` CLI：编码代理产品（扩展/技能/会话/四种运行模式）
   ↘ 依赖 pi-tui（终端 UI）、pi-mcp（MCP 客户端）、pi-codemode（沙箱 JS）

旁路：pi-durable（持久化会话/任务/文档运行时，依赖 chord + pi-ai）
      pi-env（SSH 远程执行环境，依赖 durable）
      pi-evals（行为评测）
```

几个值得注意的事实：

- **pi-coding-agent 不依赖 chord 和 durable**。交互式 `pi` 跑在内存中的 pi-agent-core 循环上，扩展系统用的是自己的 jiti 加载器；chord/durable 是面向「长时间运行、可崩溃恢复」场景的另一套运行时（供 pi-chat 等产品使用）。
- **pi-mcp 不依赖官方 MCP SDK**，是零依赖（仅 cross-spawn）的独立实现。
- **pi-codemode 只依赖 quickjs-wasi**，与 pi 其余部分完全解耦。

这个依赖图本身就是设计宣言：每一层都可以单独拿出来用（pi-ai 是独立的多提供商 SDK，pi-tui 是通用终端 UI 库，SDK 消费者可以用 `createAgentSession` 把整个编码代理嵌入自己的应用）。

## pi-ai：统一的多提供商 LLM API {#pi-ai}

pi-ai 解决的问题是：Anthropic、OpenAI、Google、Bedrock、Mistral 等各家 API 在消息格式、推理内容、缓存、用量上报上互不兼容。pi-ai 把它们归一到一套 TypeScript 类型和一个流式协议后面。

### 会话即重放日志（transcript as replay log）

核心数据模型在 `packages/ai/src/types.ts`：

```ts
// 公开输入
interface Context {
  systemPrompt?: string;
  messages: Message[];
  tools?: Tool[];
}

// 唯一合法构造途径：normalizeContext()
// 把 systemPrompt + tools 折叠为首条 system 消息
type TranscriptContext = { messages: Message[] } & Brand;

type Message =
  | SystemMessage      // content + sections? + toolsAdded?/toolsRemoved?
  | UserMessage
  | AssistantMessage   // content: (Text|Thinking|ToolCall)[]
  | ToolResultMessage;
```

最有意思的设计是 **SystemMessage 承担「补丁」语义**：会话中途的 system 消息可以通过 `sections` 替换/删除具名段落、通过 `toolsAdded`/`toolsRemoved` 增删工具。整个 `messages` 数组因此是一部重放日志——从空开始逐条重放，就能重建出任一时刻的 system prompt 与工具集。这个设计直接决定了后文会话文件的格式：磁盘上的 JSONL 不需要额外的元数据通道，消息流本身就是完整状态。

发送前有一个共享预处理 `transformMessages()`（`src/api/transform-messages.ts`）：丢弃来自其他模型的 thinking 块（签名与模型绑定）、跨行剥除 `thoughtSignature`、规范化工具调用 ID（Anthropic 限制 64 字符、OpenAI Responses 的管道 ID 哈希截断）、为非视觉模型把图片降级为文本占位符、**为孤儿 tool call 合成 `"No result provided"` 错误结果**。各家适配器（`src/api/anthropic-messages.ts`、`openai-completions.ts`、`google-shared.ts` 等）再各自做序列化，例如 Anthropic 要求 tool_result 紧跟 tool_use，连续的 ToolResultMessage 会被折叠进一条 user 消息。

### 流事件协议与「错误即数据」

统一的流式入口返回 `AssistantMessageEventStream`，事件为：

| 事件 | 含义 |
|---|---|
| `start {partial}` | 生成开始；`partial` 是被后续事件持续改写的「当前完整响应」对象 |
| `text_start` / `text_delta` / `text_end` | 文本块打开/增长/关闭 |
| `thinking_start` / `thinking_delta` / `thinking_end` | 推理块（被安全删除的推理只在 start 时完整出现） |
| `toolcall_start` / `toolcall_delta` / `toolcall_end` | 工具调用块，delta 携带 JSON 参数片段 |
| `done {reason, message}` | 成功终止，reason ∈ stop / length / toolUse / deferred |
| `error {reason, error}` | 失败终止，reason ∈ aborted / error |

关键契约：**流函数永不抛异常**。鉴权失败、网络错误、供应商 5xx 全部编码为携带 `stopReason: "error"` 的终态 `error` 事件。这使得上层（agent 循环）可以用统一的方式处理一切失败——重试、展示、写会话——而不需要 try/catch 穿透异步边界。

推理（thinking）内容的回放是个跨提供商难题：Anthropic 有签名、OpenAI 有加密的 `reasoning_details`、Google 有 `thoughtSignature`。pi-ai 把它们统一收纳到 `ThinkingContent.thinkingSignature` 这个不透明槽位里，请求时按提供商还原。

### 双轴架构：API × Provider

pi-ai 把「线协议」和「供应商」分成两个正交轴：

- **API**（`KnownApi`）：`anthropic-messages`、`openai-completions`、`openai-responses`、`google-generative-ai`、`bedrock-converse-stream`、`mistral-conversations`、`pi-messages`（pi 自己的协议）等十种，各自位于 `src/api/` 下，导出统一的 `stream`/`streamSimple` 函数，并用 `lazyApi()` 包装做到按需加载。
- **Provider**（`src/providers/`）：42 个内置工厂（`builtinProviders()`），描述端点、鉴权链（API key → OAuth → 环境变量）、动态模型列表。`createProvider()` 把两者组装起来。

各家的怪癖用模型上的 **compat 兼容位**表达（`src/providers/compat-schema.ts`），例如 OpenAI 兼容服务器的 `thinkingFormat: "zai"|"qwen"|"deepseek"|"openrouter"|...` 决定推理内容怎么塞进请求。兼容位可以从 provider id / baseUrl 自动探测。

### 模型目录：构建时生成 + 运行时覆盖

`scripts/generate-models.ts` 在构建时抓取 models.dev、OpenRouter 等目录，合并大量手工维护的覆盖项，生成 `src/providers/data/<provider>.json` 和 `models.generated.ts`（这也是仓库规则禁止手改该文件的原因）。运行时，coding-agent 里的 `withRemoteCatalog()`（`packages/coding-agent/src/core/remote-catalog-provider.ts`）再用 pi.dev 的目录做 ETag 协商覆盖——只有远端 `Last-Modified` 比本地生成时间新时才应用。模型条目携带 `contextWindow`、`maxTokens`、按百万 token 计的分层定价、`promptCache` 生命周期、`thinkingLevelMap` 等元数据。

### Token 计数：估算而非分词

pi **不内置任何真实分词器**。`src/utils/estimate.ts` 用 `字符数 / 3.5` 估算文本 token，图片固定 4800 字符；`estimateContextTokens()` 锚定在最近一条有效 assistant 消息的 `usage`（供应商上报的真实用量）上，只对之后的新消息做字符估算。这套估算只用于压缩触发和 `maxTokens` 钳制（`contextWindow − 估算 − 4096` 安全余量），计费始终以供应商回报的 `Usage`（含 cacheRead/cacheWrite/reasoning 拆分）为准。

### 提示缓存

供应商中立的 `cacheRetention: "none"|"short"|"long"` 映射到各家机制：Anthropic 在 system prompt、**最后一个工具定义**、最后一条 user 消息上打三个 `cache_control: ephemeral` 断点（经典三点布局）；OpenAI 用 `prompt_cache_key`（会话 ID 截断到 64 字符）；Google 靠请求前缀自然命中。一个精巧的细节：配合 Anthropic 的 inline-tools beta，pi 会冻结请求级工具表并注册一个永不触发的占位工具，让会话中途的工具增删不破坏已缓存前缀。

## pi-agent-core：核心 agent 循环 {#agent-loop}

`packages/agent` 只有六个源文件，却是整个 pi 的心脏。它分两层：

- **`agent-loop.ts`（约 950 行）：无状态循环**。`agentLoop(prompts, context, config, signal, streamFn)` 返回一个 `EventStream<AgentEvent, AgentMessage[]>`。
- **`agent.ts`：有状态的 `Agent` 门面**。持有会话、steering 队列、订阅者，把循环包成 `prompt()` / `continue()` / `steer()` / `abort()` 的命令式 API。

### 循环的精确形状

下图是交互式流程图（点击节点可查看源码行号，可在[新标签页打开](/diagrams/pi-agent-loop.html)）：

<figure>
  <iframe src="/diagrams/pi-agent-loop.html" title="pi Agent 循环交互图" loading="lazy" style="width:100%;height:900px;border:0;"></iframe>
  <figcaption>pi Agent 循环：一次模型请求加一轮工具执行构成内层循环；steering 在轮次间注入，无工具调用且队列为空时 run 结束。</figcaption>
</figure>

`runLoop` 是一个双重 while 循环。内层循环一次迭代 = 一次模型请求 + 一轮工具执行：

1. `prepareNextTurn` 钩子（第二轮起）：上轮结束后、下轮请求前的准备点——压缩这类耗时操作就挂在这里。
2. 排空待处理输入：新消息进入上下文，发出 `message_start`/`message_end`。
3. `prepareRequest` 钩子：**每次**模型请求前的最后修改点（可换上下文/模型/思考等级）。
4. `streamAssistantResponse`：调 LLM。流式部分消息在 `start` 时就立即进入上下文，每个 delta 更新它，`done` 时替换为最终消息——所以 UI 和持久层看到的是同一份逐字增长的对象。
5. 终态检查：`stopReason` 为 error/aborted 时无条件结束本轮与本 run。
6. 工具调用：若助手消息含 tool call：
   - `stopReason === "length"`（输出被截断）时，**整批工具调用全部标记失败**并不执行——截断流的工具参数可能「能通过校验但悄悄不完整」，pi 宁可让模型重发。
   - 否则执行工具（见下节），结果作为 ToolResultMessage 入上下文，回到第 1 步。
7. `finishTurn` 钩子：可返回 `{action: "end"}` 强制结束或 `{action: "continue"}` 强制续跑。
8. 轮询 steering 队列：有则作为下轮输入。

内层退出（无工具调用、无 steering）后，外层轮询 follow-up 队列，或处理显式 continuation，否则发出 `agent_end` 结束。

**停止条件用一句话概括**：当一轮既没有产生工具调用、steering 和 follow-up 队列都为空、且 `finishTurn` 没有强制继续时，run 结束。

### 事件流与背压

循环对外只发出 `AgentEvent`：`agent_start` → 每轮 `turn_start` → `message_start/update/end`（update 携带底层 `assistantMessageEvent`）→ `tool_execution_start/update/end` → `turn_end` → `agent_end`。

裸 `agentLoop()` 流是「观察性」的：保序但不等待异步消费者。`Agent` 门面则逐个 await 订阅者——`message_end` 因此成为工具预检前的屏障，保证扩展在工具执行前一定能处理完完整消息。这个差异对写扩展的人重要：门面模式下异步 handler 会真正阻塞循环。

### 工具管线与权限门

每个工具调用经过固定管线（`agent-loop.ts`）：

```
tool_execution_start
  → 解析工具（未注册 → 立即错误结果 "Tool <name> not found"）
  → validateToolArguments（TypeBox 校验 + JSON Schema 宽松转换）
  → beforeToolCall 钩子        ← 权限门：返回 {block: true, reason} 则不执行
  → tool.execute(id, args, signal, onUpdate)
  → afterToolCall 钩子          ← 可逐字段改写结果（content/isError/usage/terminate）
tool_execution_end
```

`beforeToolCall` 就是 pi 的权限系统挂载点——注意 pi 内核**没有**内置权限策略，`packages/agent` 只提供钩子；coding-agent 默认不拦截，容器化方案（Docker、micro-VM）在进程外做边界。工具失败约定为**抛异常**，由循环捕获转为 `isError: true` 的结果。

并发模型：批次默认并行——预检（校验 + beforeToolCall）串行执行后，`execute` 用 `Promise.all` 并发；`tool_execution_end` 按**完成顺序**发出，而工具结果消息按**助手消息中的出现顺序**进入上下文（保证模型看到确定顺序）。任何工具声明 `executionMode: "sequential"` 则整批串行。工具结果带 `terminate: true` 投票，全票通过才提前终止 run。

### Steering：运行中转向

用户在模型流式输出或工具执行期间输入的消息进入 `steeringQueue`，循环在三个安全点轮询：本轮开始、`prepareNextTurn` 后、`finishTurn` 后——即**当前回合的工具调用永不被打断**，新输入在下次模型请求前注入。run 结束后才处理的输入走 `followUpQueue`。队列有 `all` 和 `one-at-a-time` 两种排水模式（默认后者，避免一次注入多条把模型带偏）。

### 传输抽象

agent 包对 LLM 的全部认知浓缩为一个函数类型：

```ts
type StreamFn = (model, context: TranscriptContext, options?)
  => AssistantMessageEventStream;
```

默认实现由宿主通过 `setDefaultStreamFn()` 注入（coding-agent 传入 pi-ai 的 `models.streamSimple`）。另有一个 HTTP 代理实现 `streamProxy()`：POST 到远端 `/api/stream`，把带宽裁剪过的 SSE 事件在客户端重建为完整流（含用增量 JSON 解析器重组工具参数）——这就是 pi 能跑在浏览器里的原因。

## pi-coding-agent：把循环变成产品 {#coding-agent}

`packages/coding-agent` 是 CLI 本体。中枢是 `src/core/agent-session.ts`（约 4400 行的 `AgentSession` 门面）：组合核心 `Agent`、工具注册表、扩展运行器、压缩与重试状态机、会话边界事件。核心 `Agent` 保持对扩展和产品逻辑一无所知。

### 启动流程

`pi` 回车之后（`src/main.ts`）：

1. 参数解析与模式裁决：`rpc` > `json` > `print`（`--print` 或 stdin/stdout 非 TTY 时）> `interactive`。
2. 迁移与设置加载：`~/.pi/agent/settings.json` 与项目 `.pi/settings.json` 深合并。
3. 会话管理器：会话文件位于 `~/.pi/agent/sessions/--<cwd-编码>--/<时间戳>_<uuid>.jsonl`；处理 `--continue`/`--resume`/`--fork`。
4. **项目信任**：`.pi/` 下的扩展、设置在项目获得信任前不加载——这是 pi 的安全边界（内核无权限系统，靠信任决议 + 进程权限 + 可选容器化）。
5. 运行时组装：`ModelRuntime` 读取 `~/.pi/agent/auth.json`（密钥/OAuth）与 `models.json`（自定义提供商），`DefaultResourceLoader` 加载扩展、技能、提示词模板、主题、AGENTS.md、SYSTEM.md。
6. 模型解析优先级：CLI `--model` > 作用域模型 > 会话记忆 > 设置默认值；思考等级同理。
7. `createAgentSession()`（`src/core/sdk.ts`，即公开 SDK 的入口）创建核心 Agent 并接线，最后按模式分发到交互式 TUI / print / JSON / RPC。

### 系统提示词：分节组装

`src/core/system-prompt.ts` 把 system prompt 组织成有序的具名段落：`preamble`（基础指令，可被 `--system-prompt` 或 SYSTEM.md 整体替换）、`<tools>`、`<rules>`（按启用的工具条件拼接）、`<docs>`、`<addendum>`（APPEND_SYSTEM.md）、`<project_context>`（AGENTS.md 渲染为 `<project_instructions path=...>`，从 cwd 走到根逐层发现，候选名含 CLAUDE.md）、`<skills>`、`<cwd>`，再加扩展自定义段落。

妙处在于**增量同步**：每次请求前对段落做 diff，变化以 `SystemMessage.sections` 补丁写入会话——与 pi-ai 的重放日志设计严丝合缝。会话中途换技能、改工具，只是消息流里多了一条补丁。

### 内置工具与工具暴露模型

默认工具集 `read / bash / edit / write`，另有 `grep / find / ls / powershell`（`src/core/tools/`）。细节值得注意：grep 优先用托管安装的 ripgrep；变更类工具经 `withFileMutationQueue` 串行化避免编辑互踩；每个工具除了实现还有 `ToolExposure`——`direct`（模型可直接调用）、`model-only`、`codemode`（仅沙箱内可用）、`deferred`、`hidden`——这是工具编排（如 tool_search 动态装载）的基础。嵌套工具调用（工具内调工具，ID 形如 `<parent>/<n>`）经 `ctx.executeTool` 走同一套钩子管线。

### 扩展系统

扩展是 TypeScript 模块，用 **jiti** 加载进 pi 进程（`src/core/extensions/loader.ts`，别名机制让扩展 `import "@earendil-works/pi-ai"` 时拿到宿主正在运行的同一份实例）。来源：`~/.pi/agent/extensions/`、受信项目的 `.pi/extensions/`、`pi -e` 临时加载、以及 **pi 包**（`pi install npm:…`/`git:…`，按 package.json 的 `pi` 清单或约定目录提供扩展/技能/模板/主题）。

`ExtensionAPI`（`src/core/extensions/types.ts`）的事件面几乎覆盖了本文提到的每个钩子点：会话生命周期、`before_agent_start`（可改 system prompt）、`context`（改请求上下文）、`tool_call`（可阻断，即权限）、`tool_result`（可改写）、`provider_stream_event`（裸供应商事件）……还可以注册工具、斜杠命令、快捷键、自定义消息渲染器乃至完整 TUI 组件。内置扩展本身也是可替换的——第三方扩展注册 `/mcp` 会顶掉内置的。

### 技能：渐进披露

技能是带 YAML frontmatter（`name`、`description`）的 `SKILL.md` 目录（`src/core/skills.ts`），发现路径包括 `~/.pi/agent/skills`、`.pi/skills`、Agent 规范的 `~/.agents/skills` 等。**只有名字、描述和文件位置进入 system prompt 的 `<skills>` 段**；模型判断任务匹配时用 read 工具按需加载正文。这个渐进披露设计让几百个技能只花几百 token 的常驻成本。声明 `disable-model-invocation` 的技能则只能由用户经 `/skill:name` 手动展开。

### 会话：追加式树

会话文件是 JSONL（`src/core/session-manager.ts`）：首行 `SessionHeader`，其后每行一个 `{type, id, parentId, timestamp}` 条目——**追加式树**。条目类型有 `message`、`model_change`、`thinking_level_change`、`compaction`、`branch_summary`、`custom`（扩展状态，不进模型上下文）、`context_edit` 等。当前叶子决定活跃分支，从叶子回溯到根即得模型上下文。在历史节点继续对话 = 原地长出新分支；`/fork` 把选中历史复制进新文件。

`context_edit` 条目值得单独一提：它**不修改**被指向的历史条目，而是追加一条「替换/略过」记录，投影时套用。这让 `/context` 树选择器可以在不破坏审计性的前提下把某些条目排除出模型上下文，错误恢复也用它自动略过失败的回合。

### 压缩（compaction）

压缩策略在 `src/core/compaction/compaction.ts`，触发条件一句话：

```
contextTokens > contextWindow - reserveTokens   // 默认 reserve 16384
```

流程：以最近 assistant usage 为锚估算 token → `findCutPoint()` 从尾部回溯累计 `keepRecentTokens`（默认 20000），只在 user/assistant 边界下刀（绝不切在 tool result 上）→ 用固定模板（Goal / Constraints / Progress / Key Decisions / Next Steps / Critical Context）让模型生成摘要 → 追加 `compaction` 条目（含摘要、保留起点、完整 system 快照）。原始条目留在树里，只是后续请求不再投影它们。供应商报上下文溢出时还有一条恢复路径：自动压缩一次然后重试。分支切换时的分支摘要（`branch_summary`）走同一管道。

### 四种运行模式

交互式 TUI、print（单发取最终文本）、JSON（`--mode json`，JSONL 事件流）、RPC（`--mode rpc`：stdin 收 JSONL 命令，stdout 回响应 + 会话事件；命令面覆盖 prompt/steer/abort/换模型/压缩/会话树遍历等，扩展 UI 请求也被转发）。四种模式共享同一套 agent 与会话机制，这也是 RPC 宿主（如 OpenClaw）能把 pi 当后端用的原因。

## pi-tui：差分渲染的终端 UI {#tui}

`packages/tui` 是独立终端 UI 库，组件模型极简：

```ts
interface Component {
  render(width: number): string[];   // 每行不得超过 width，否则 TUI 崩溃并写日志
  handleInput?(data: string): void;  // 可选
  invalidate(): void;
}
```

整个界面是组件树，根组件 `TuiBase` 持有焦点系统与浮层栈。两个渲染器：`TuiMainScreen`（常规模式，保留终端回滚区）和 `TuiAltScreen`（全屏模式，flexbox 风格布局 + 应用自管滚动），交互式 pi 用后者。

**差分渲染算法**（`tui-main-screen.ts`）：每帧把组件树渲染成行数组，与上一帧逐行比对找出 `firstChanged..lastChanged` 区间，只对该区间重写（每行先 `\x1b[2K` 清行）；纯追加场景直接 `\r\n` 下滚。宽度变化、内容缩回、变更触及已滚出视口的区域时才全量重绘。输出包裹在 CSI 2026 同步输出协议里避免撕裂，经有界写入器分块（规避 V8 字符串长度限制）。渲染按 16ms 最小间隔合并，键盘输入则走 `requestImmediateRender` 保证低延迟。

输入侧：原始模式 stdin + 括号粘贴 + **Kitty 键盘协议协商**（失败则回退 modifyOtherKeys）；`StdinBuffer` 以 10ms 转义超时重组批量输入。少量原生模块（剪贴板、修饰键检测）只在 darwin/win32 加载，其余全是 ANSI/OSC 序列。光标用一个 APC 私有序列做标记（`CURSOR_MARKER`），渲染后定位真正的硬件光标。

## Codemode：让模型写程序而不是发工具调用 {#codemode}

`packages/codemode` 是 pi 最有辨识度的机制：模型不再逐个发起 tool call，而是**写一段 JavaScript 程序**，程序里以函数调用的方式使用工具。

- 沙箱是 **QuickJS 编译到 WASM**，唯一宿主入口一个；`fetch`、`process`、`require`、定时器、模块系统全部不存在——**调用注入的工具是脚本唯一的能力**。
- 工具的 JSON Schema 被 `renderDeclarations()` 渲染成带注释的 TypeScript 声明，直接放进 codemode 工具的 description 里，模型照着写代码。
- 宿主与 VM 用消息协议通信（`call`/`result`/`output`/`done`），工具在宿主侧执行。硬限制：输出 16Mi 字符、store 每值 256Ki。
- 关键收益：**嵌套工具调用不进入 LLM 上下文**。脚本里循环调用一百次工具，模型只看到脚本输出和返回值。批处理任务的 token 成本从 O(调用数) 降到 O(1)。
- 在 coding-agent 里，脚本中的工具调用经 `ctx.executeTool` 走完整管线（校验、权限钩子、用量统计一样不少）；在 durable 里更进一步——沙箱是**确定性模式**（虚拟时钟 + 种子 PRNG + 严格按序投递结果），每次结果投递都记进 journal，进程崩溃后重放脚本、按 journal 回放结果，嵌套工具调用按 key 重挂到原任务，一个都不重跑。

## chord 与 durable：另一套运行时 {#chord-durable}

这两章属于 pi 生态里服务长期运行场景的部分，交互式 CLI 不用它们，但体现了同一批设计思想的延伸。

**chord**（`packages/chord`）解决的问题：把应用建模为分布在多进程（服务器、worker、浏览器标签）里的对象，支持运行中热重载。核心概念是**契约**（contract，带版本的对象类型，placement 是声明的一部分）、**引用**（本地远程行为一致的 Ref）、**cell**（可观察状态源）与**增量复制**（结构性 diff 产生 ops：字符串追加、数组 splice 等）。热重载分五阶段：加载 → Prepare（新工厂在现有对象上无副作用试跑，失败即安全中止）→ Gate（仅当 cell 版本提升或独占资源变更时等在途操作排空并跑 `migrate`）→ Commit（一步原子换码）→ Retire（旧代码服务完已开始的根）。

**pi-durable**（`packages/durable`）是在 chord + pi-ai 之上的持久化运行时，不变量是 **commit-before-show**：任何东西在提交落盘前不可见。会话是追加式不可变条目流；**任务**是分阶段状态机，每步落 checkpoint，重启后从提交点续跑；存储后端有内存、SQLite（WAL + `synchronous=NORMAL`）、JSONL、Cloudflare Durable Object。崩溃循环的任务在 5 次重启后被停放。可以把 durable 理解为「pi-agent-core 的崩溃安全版本」：同样的 agent 语义，完全不同的持久化承诺。

## 其余角色 {#others}

- **pi-mcp**：零依赖 MCP 客户端（stdio / streamable-http / 内存传输，含 OAuth 流程）。服务器工具包装成 `mcp__<server>__<tool>` 命名的 agent 工具，也可在 codemode 脚本里当普通函数调用。
- **pi-env**：面向 durable 的远程执行环境——一个 Rust 守护进程经 SSH 部署到目标机，agent 的文件/进程工具跑在远端，存储与凭据留在本地。
- **pi-telemetry**：所有包共用的遥测契约。刻意**显式传递 Context/span、不用 AsyncLocalStorage**——每个函数多一个参数，换来边界清晰和可测试性；各域包（pi-ai、durable）在上面发布自己的类型化 schema。
- **pi-evals**：基于 vitest-evals 的行为评测，含「文档提升」评测——同一用例在有/无文档的两个 Docker 容器里各跑一遍，报告文档带来的通过率提升。

## 设计观察 {#observations}

通读源码后，几条贯穿性的设计决策值得记录：

**钩子而非策略**。内核（pi-agent-core）对压缩、权限、重试、系统提示词一概只提供挂载点，策略全部住在消费方。代价是组装代码多，收益是同一个内核能同时支撑 CLI、SDK 嵌入、RPC 后端和 durable 这样的另一套运行时——而这四种宿主确实共存于这个仓库。

**会话即重放日志**。system prompt 段落补丁、工具增删、模型切换全部作为带内消息进入同一部日志，磁盘 JSONL 因此是唯一事实源，没有旁路状态。`context_edit` 也遵守这个纪律——修改历史的方式是追加记录。

**错误即数据**。从 pi-ai 的流协议（永不抛异常，错误是 `stopReason: "error"` 的终态消息）到 agent 循环把截断批次的工具调用整体判失败，失败路径都是一等公民，可被重试层、扩展和会话层统一消费。

**估算优于精确，锚定优于重算**。token 不分词、按字符估算、锚在最近一次供应商回报的 usage 上——只在确实需要便宜可逆的决策（何时压缩）时使用，计费和限流仍用真实数字。

**最小内核的产品观**。没有内置子代理、没有计划模式、没有权限系统，README 的说法是「让 Pi 造你想要的东西，或者装一个合你胃口的包」。内置扩展（codemode、mcp、tool-search）可被第三方整体替换这件事，大概是这个立场最诚实的表达。

## 参考 {#references}

- 源码：[earendil-works/pi](https://github.com/earendil-works/pi)，本文分析基于 v1.3.0
- 官方机制文档：`packages/coding-agent/docs/how-pi-works.md`
- 各包文档：`packages/coding-agent/docs/`（extensions、skills、session-format、rpc、sdk 等）
- 本站 [Pi 用户手册](/book/pi/)（翻译自官方文档的使用层手册，与本文的源码层视角互补）
