核心架构与设计哲学
本文档讨论 Pi 为什么这样设计,而不只是列出目录和接口。以 Pi v0.80.10 源码为基准。建议先读 从一个最小 Agent 开始,再回来理解下面这些取舍。
先从一个任务想起
用户说:“找到项目里的登录错误,修好它,并运行测试。”
一个只会聊天的程序只能给建议;一个 Agent 需要在模型和程序之间来回传递:模型决定先搜索什么,程序执行搜索,结果回到模型,模型再决定是否修改文件和运行测试。Pi 的大部分架构,都是在回答三个问题:
- 模型如何保持可替换?
- 工具和扩展如何不把核心循环变成一团耦合代码?
- 一个可能执行代码的 Agent,如何在陌生项目里保持可控?
后面的 Models、事件、扩展、项目信任和会话设计,都可以看作这三个问题的不同答案。
一、关注点分离:四个核心包
Pi 被拆成四个包,每层都有清晰边界:
┌─────────────────────────────────────────────────────────────┐
│ pi-tui │
│ 终端 UI 库:差分渲染、组件、编辑器、事件分发 │
├─────────────────────────────────────────────────────────────┤
│ pi-ai │
│ LLM 通信层:Models 运行时、Provider factory、API 实现、认证 │
├─────────────────────────────────────────────────────────────┤
│ pi-agent-core │
│ 智能体运行时:Agent Loop、工具调用、事件流、状态管理 │
├─────────────────────────────────────────────────────────────┤
│ pi-coding-agent │
│ 应用层:CLI、项目信任、扩展、会话、TUI 集成、工具实现 │
└─────────────────────────────────────────────────────────────┘
这种分层让每一层都可以独立测试、替换或复用。例如你可以用 pi-agent-core + pi-ai 写一个没有 TUI 的脚本,也可以用 pi-tui 做其他终端应用。
二、Models 运行时与 Provider 抽象
Pi 的 pi-ai 从全局 API registry 转向 Models 运行时;在 coding-agent 中,这个能力进一步由 ModelRuntime 组合为应用级模型门面。
旧架构(v0.78 及以前)
调用者
→ stream(model, context, opts) // 全局函数
→ api-registry 根据 model.api 查找 ApiProvider
→ ApiProvider.stream(model, context, opts)
问题:
- 全局注册表有副作用,tree-shaking 困难
- 认证在调用侧临时注入,难以管理 OAuth 刷新
- 所有模型元数据塞在一个巨型
models.generated.ts 中
当前架构(v0.80.10)
调用者
→ models = createModels({ credentials, authContext })
→ models.setProvider(anthropicProvider())
→ models.setProvider(openAIProvider())
→ models.stream(model, context, opts)
→ Models 运行时根据 model.provider 找到 Provider
→ Provider 自己解析 auth(CredentialStore + AuthContext)
→ Provider.api.stream(model, context, opts)
核心抽象:
详细实现见 pi-ai:Models 运行时与 Provider 架构。
事件流抽象依然不变
无论底层是 OpenAI SSE、Anthropic SSE、Google 流式还是其他协议,pi-ai 都把它们统一成同一组事件:
type AssistantMessageEvent =
| { type: 'start'; partial: AssistantMessage }
| { type: 'text_start'; contentIndex: number }
| { type: 'text_delta'; delta: string; contentIndex: number }
| { type: 'text_end'; content: string; contentIndex: number }
| { type: 'thinking_start'; contentIndex: number }
| { type: 'thinking_delta'; delta: string }
| { type: 'thinking_end'; content: string }
| { type: 'toolcall_start'; contentIndex: number }
| { type: 'toolcall_delta'; delta: string; partial: AssistantMessage }
| { type: 'toolcall_end'; toolCall: ToolCall }
| { type: 'done'; reason: StopReason; message: AssistantMessage }
| { type: 'error'; reason: 'error' | 'aborted'; error: AssistantMessage };
Agent Loop 代码完全不需要关心是哪个 Provider。它只处理这些标准事件。
三、事件驱动架构
Pi 的整个架构是事件驱动的。从 LLM 流式响应到 TUI 更新,从工具执行到扩展触发,全部通过事件传递。
Agent 层事件
AgentEvent
├── agent_start — Agent 开始处理
├── agent_end — Agent 处理完成
├── turn_start — 一轮 LLM+工具 开始
├── turn_end — 一轮结束
├── message_start — 消息开始
├── message_end — 消息结束
├── message_update — 消息更新(流式 delta)
│ └── assistantMessageEvent — 嵌套 LLM 原始事件
├── tool_call_start — 工具调用开始
├── tool_call — 工具调用
├── tool_result — 工具结果
├── compaction_start — 上下文压缩开始
├── compaction_end — 上下文压缩结束
└── auto_retry_start / end — 自动重试
事件流示例
用户输入 "解释这个文件" 后的事件序列:
1. agent_start
2. turn_start
3. message_start { role: "user", content: "解释这个文件" }
4. message_end
5. message_start { role: "assistant" }
6. message_update { delta: "好的" }
7. message_update { delta: ",让" }
8. ...
9. tool_call_start { name: "read" }
10. tool_call { name: "read", args: { path: "..." } }
11. tool_result { content: "文件内容..." }
12. turn_end
13. turn_start
14. message_update { delta: "这个文件..." }
15. ...
16. agent_end
TUI 监听这些事件并实时更新界面。
扩展事件
扩展系统有自己的事件体系,扩展可以在多个环节拦截或增强行为:
type ExtensionEvent =
| 'input'
| 'before_agent_start'
| 'message_end'
| 'tool_call'
| 'tool_result'
| 'before_provider_request'
| 'after_provider_response'
| 'agent_end'
| 'session_shutdown'
| 'project_trust' // 项目信任决策
| 'session_before_compact' // 压缩前事件,包含 reason/willRetry
| 'session_compact';
四、TUI 差分渲染
文件:packages/tui/src/tui.ts
传统终端 UI 每帧清屏重绘会闪烁且性能差。Pi 的解决方案是差分渲染:
- 保存上一帧的输出(
previousLines: string[])
- 渲染当前帧
- 逐行对比,只输出变化的行
class TUI extends Container {
private previousLines: string[] = [];
private previousWidth = -1;
private previousHeight = -1;
private renderRequested = false;
private lastRenderAt = 0;
requestRender(force = false): void {
if (force) {
this.previousLines = [];
this.previousWidth = -1;
this.previousHeight = -1;
}
this.renderRequested = true;
process.nextTick(() => this.scheduleRender());
}
private scheduleRender(): void {
if (this.stopped || this.renderTimer || !this.renderRequested) return;
const elapsed = performance.now() - this.lastRenderAt;
const delay = Math.max(0, TUI.MIN_RENDER_INTERVAL_MS - elapsed);
this.renderTimer = setTimeout(() => {
this.renderTimer = undefined;
this.renderRequested = false;
this.lastRenderAt = performance.now();
this.doRender();
}, delay);
}
}
组件接口
interface Component {
render(width: number): string[];
handleInput?(data: string): void;
invalidate(): void;
}
所有 UI 元素都是 Component:
Text — 纯文本行
Container — 子组件容器(递归渲染)
Editor — 编辑器(含光标、选区)
Markdown — Markdown 渲染
Loader — 加载动画
SelectList — 选择列表
Overlay 系统
Pi TUI 支持模态覆盖层:
ui.showOverlay(new ModelSelectorComponent(), {
anchor: 'center',
width: 60,
height: 20,
});
Overlay 渲染时先渲染基础内容,再叠加 overlay;关闭时只更新被覆盖区域。
五、扩展系统
文件:packages/coding-agent/src/core/extensions/runner.ts
扩展是一个 TypeScript 模块,通过 ExtensionFactory 函数创建。扩展可以:
- 注册自定义工具 —
pi.registerTool({ ... })
- 注册斜杠命令 —
pi.registerCommand({ ... })
- 监听事件 —
pi.on('tool_call', handler)
- 拦截输入 —
pi.on('input', handler)
- 修改系统提示 — 在
before_agent_start 中修改
- 创建 UI 组件 —
ctx.ui.setWidget() / ctx.ui.setStatus()
- 注册快捷键 — 自定义按键绑定
- 参与项目信任决策 —
pi.on('project_trust', handler)
扩展加载流程
1. discoverAndLoadExtensions()
├── 扫描 ~/.pi/agent/extensions/ (全局)
├── 扫描 .pi/extensions/ (项目,仅在项目被信任后加载)
├── 扫描已安装的 Pi Packages
└── 加载 CLI --extensions 指定的路径
2. 对每个扩展:
├── import(filePath)
├── 调用 ExtensionFactory(cwd, agentDir)
└── 注册工具、命令、事件监听
3. ExtensionRunner 汇总所有扩展
├── 工具注册到 ToolRegistry
├── 命令注册到 SlashCommands
└── 事件监听注册到事件总线
输入拦截示例
pi.on('input', (event) => {
const match = event.text.match(/#(\d+)/);
if (match) {
const issueUrl = `https://github.com/${repo}/issues/${match[1]}`;
return {
action: 'transform',
text: event.text.replace(/#\d+/, `[Issue #${match[1]}](${issueUrl})`),
};
}
return { action: 'pass' };
});
六、项目信任与安全边界
当前 Pi 使用项目信任(project trust):在加载项目本地 .pi 目录、扩展、资源前,会先请求用户确认。
信任流程:
启动时
→ 检查 cwd 是否有需要信任的项目资源
→ 触发 project_trust 扩展事件
→ 若未决策,弹出 TUI 选择器
→ 用户选择:始终信任 / 仅本次 / 从不
→ 决策写入 trust store
→ 继续加载项目本地扩展和资源
这防止了恶意项目的 .pi/extensions 或 AGENTS.md 在不被察觉的情况下执行。
详见 项目信任与认证体系。
七、设计哲学总结
1. 关注点分离
pi-ai → LLM 通信(怎么跟大模型说话)
pi-agent-core → 智能体逻辑(怎么循环、怎么调用工具)
pi-coding-agent → 应用层(CLI、扩展、会话、TUI 集成、项目信任)
pi-tui → 终端 UI(怎么在终端里显示和交互)
2. 事件驱动而非回调嵌套
// ✅ 事件流(线性、可组合)
for await (const event of stream) {
emit(event); // 所有监听者收到
}
3. 类型安全贯穿
const model = getBuiltinModel('anthropic', 'claude-sonnet-4');
// ^ Model<'anthropic-messages'>
models.stream(model, context, {
// TypeScript 知道这里是 AnthropicMessagesStreamOptions
maxTokens: 4096,
});
4. 无状态优于全局副作用
pi-ai 的 Models 与 Provider factory 仍然是可独立创建的运行时对象;coding-agent 则由 ModelRuntime 持有并组合内置 Provider、配置文件、扩展 Provider 和凭证。这让多个 Agent session 可以拥有独立的模型配置,也便于测试。
5. 扩展优于修改
不鼓励修改核心代码。通过扩展系统:
- 添加自定义工具
- 注册斜杠命令
- 监听/修改事件
- 创建 UI 组件
- 参与项目信任决策
6. 渐进式复杂度
简单使用:pi "解释这个文件"
进阶使用:pi --model anthropic/claude-sonnet-4
高级使用:自定义扩展、自定义 Provider、修改 TUI
下一步