设计启发

这一章把前面分析过的源码设计抽象成可迁移的工程模式。它不依赖 Pi 本身,适合做其他 agent runtime、coding agent、插件系统时参考。

每条设计启发背后的具体源码入口见源码索引

1. Agent Loop 要小

Pi 最值得借鉴的一点是把 agent-loop.ts 做得很小。它只处理:

  • 消息进入上下文。
  • 模型流式响应。
  • 工具调用和工具结果。
  • steering/follow-up 队列。
  • 生命周期事件。

所有产品特性都放在外层。这样 loop 可以被 CLI、SDK、RPC、测试和未来产品复用。

2. 自定义消息要有转换边界

coding agent 一定会产生不属于 LLM 协议的消息:

  • shell command output。
  • 压缩摘要。
  • 分支摘要。
  • 扩展私有消息。
  • UI-only notification。

Pi 的做法是允许 AgentMessage 扩展,但在 provider 边界统一 convertToLlm()。这比一开始就把所有消息压扁成 user/assistant 更灵活,也比让 provider 知道产品消息更干净。

3. 会话存储应保留结构

很多 agent 会把会话存成线性聊天数组。Pi 用 JSONL append-only tree:

  • 可以 fork。
  • 可以切换 leaf。
  • 可以保留原始历史。
  • compaction 不需要删除旧消息。
  • branch summary 有自然位置。

如果你的 agent 需要探索式开发、回滚、分支对比,树形会话比数组更适合。

4. 上下文不是历史

Pi 把“历史如何存储”和“模型看到什么”分开:

  • 历史是 JSONL entry。
  • 模型上下文是 buildSessionContext() 生成的视图。
  • 压缩只是改变视图,不改写原始历史。

这是长会话 agent 的关键原则。

5. 工具执行要兼顾三种顺序

工具系统至少有三种顺序:

  • preflight 顺序:校验、权限、hook。
  • execution 顺序:可以并行以提高速度。
  • context 写回顺序:必须稳定可重放。

Pi 的设计把这三种顺序分开处理,尤其适合多 tool call 的模型。

6. 扩展事件需要语义化

不要只提供一个通用 emit()。不同事件应该有不同规则:

  • 有的事件可 cancel。
  • 有的事件可 block。
  • 有的事件可替换 message。
  • 有的事件串行累积 result。
  • 有的事件只是通知。

Pi 的 ExtensionRunner 明确区分这些事件,使扩展强大但不混乱。

7. 插件上下文要可失效

支持 reload、switch session、fork 的系统里,插件很容易持有旧 context。Pi 在 dispose 时让旧 extension context 失效,并要求扩展在新 session 回调中继续工作。

这是插件系统稳定性的关键细节。

8. Provider 差异要显式建模

“OpenAI-compatible” 往往只是大体兼容。Pi 的 compat 配置把差异写成模型/provider metadata:

  • role 支持差异。
  • tool result 格式差异。
  • thinking 参数格式差异。
  • cache control 差异。
  • session affinity 差异。

这比到处写 if/else 更容易维护。

9. 功能不一定都进核心

Pi 明确不内置 MCP、sub-agent、plan mode、permission popup、todo、background bash,而是让扩展实现。这个取舍背后是一个产品架构原则:

核心只提供稳定能力边界,工作流由插件组合。

这能减少核心复杂度,也给用户更大的工作流自由度。

10. 摘要要保留工程上下文

对 coding agent 来说,摘要不能只有“聊了什么”。Pi 的 compaction summary 还保留:

  • 目标。
  • 约束。
  • 已完成/进行中/阻塞。
  • 关键决策。
  • 下一步。
  • 关键上下文。
  • read files。
  • modified files。

这是让另一个 LLM 继续工作的最低有效上下文。

一句话总结

Pi 的优秀设计不是某个单点技巧,而是边界感:模型协议、agent loop、产品会话、扩展系统、工具执行、上下文压缩各自有清晰职责,并通过少数稳定接口连接起来。