Pi 当前架构:module、interface、seam 与状态所有权
Pi 的当前架构可以从四个问题理解:module 分别承担什么职责,通过哪些 interface 协作,状态由谁拥有,哪些 seam 真的可以替换。
源码快照:Pi
ccb9464114c2542aad444054f590f06b03a324ac(2026-08-28)。历史全景见Pi 如何演变成今天的架构,关键 runtime 链条见源码考古专题。
四个观察角度
Section titled “四个观察角度”架构不能只从目录名判断。阅读一个系统时,至少要同时画四张图:
- 依赖图:谁在编译或加载时知道谁;
- 控制流图:运行时是谁调用谁,回调和事件又向哪里返回;
- 状态图:每一份可变状态由谁拥有,在哪个生命周期内有效;
- 证据图:一个 interface 是否真的拥有多个 adapter、真实调用者和独立测试。
本章采用以下架构术语:
- module:具有一个 interface 和一份 implementation 的代码单元,可以是函数、class、package 或跨层切片;
- interface:调用者正确使用 module 必须知道的全部事实,不只包括 TypeScript 类型,还包括顺序、失败、取消和性能约束;
- seam:调用者无需修改原位置,就能替换行为的接入位置;
- adapter:在某个 seam 上满足 interface 的具体实现;
- depth:调用者学习较小 interface 后,可以获得多少被隐藏的行为;
- leverage:同一个 module 被多少调用者和测试复用;
- locality:某类变化是否主要停留在一个位置,而不是扩散到所有调用者。
一个 package 不是因为独立发布就自动成为深 module。反过来,一个 class 或函数也可能在一个很小的 interface 后隐藏大量复杂行为。
先看全仓库,再收敛到运行主链
Section titled “先看全仓库,再收敛到运行主链”当前仓库不只有三个 package。下面是 package manifest 能直接证明的内部依赖;箭头表示左侧 package 依赖右侧 package,不表示运行时事件方向。
flowchart LR
Coding[pi-coding-agent] --> Agent[pi-agent-core]
Coding --> AI[pi-ai]
Coding --> Client[pi-client]
Coding --> Protocol[pi-protocol]
Coding --> TUI[pi-tui]
Agent --> AI
Agent --> Telemetry[pi-telemetry]
AI --> Telemetry
Client --> Protocol
Server[pi-server] --> Protocol
Server --> AI
SQLite[sqlite session backend] --> Agent
SQLite --> AI
Evals[pi-evals] --> Coding
Evals --> AI
来源是各 package 的 package.json :: dependencies。这张图可以分成两部分:
- 运行主链:
pi-coding-agent → pi-agent-core → pi-ai; - 外围 adapter、宿主与质量反馈:TUI、远程 protocol/client/server、SQLite session backend、telemetry 和 evals。
主链适合建立第一版心智模型,外围 package 则用于检验主链是否真的留下了可替换 seam。
第一层:pi-ai 统一模型协议,不拥有 Agent 决策
Section titled “第一层:pi-ai 统一模型协议,不拥有 Agent 决策”packages/ai 定义 Model、Context、Message、Tool、AssistantMessageEvent、StopReason 和 EventStream 等共享数据契约,并提供各 provider/API adapter。
它主要隐藏三类差异:
- 不同 provider 的请求字段、认证和 reasoning 参数;
- 不同 wire format 如何变成统一 assistant event;
- 工具参数如何使用运行时 schema 校验。
关键来源:
packages/ai/src/types.ts :: Model, Context, Message, Tool, AssistantMessageEventpackages/ai/src/utils/event-stream.ts :: EventStream, AssistantMessageEventStreampackages/ai/src/utils/validation.ts :: validateToolArgumentspackages/ai/src/providers/与packages/ai/src/api/packages/ai/src/providers/faux.ts
它的 interface 为什么有 leverage
Section titled “它的 interface 为什么有 leverage”Agent Loop 只消费统一的 AssistantMessageEventStream,不用在工具循环中反复判断 Anthropic、OpenAI 或 Google 的原始事件格式。测试还能使用 faux provider 或可控 stream 代替真实网络。这说明 provider seam 至少有生产 adapter 和测试 adapter,不只是为未来预留的抽象。
它没有隐藏什么
Section titled “它没有隐藏什么”统一协议没有让 provider 差异消失。模型能力、认证、缓存、重试、reasoning 选项和原始 payload hook 仍会进入公开类型或专用子路径。packages/ai/package.json :: exports 同时公开根入口、./providers/*、./api/* 与 ./compat;这是能力,也是需要维护的兼容面。
第二层:pi-agent-core 拥有 Agent 运行语义
Section titled “第二层:pi-agent-core 拥有 Agent 运行语义”packages/agent 的核心不是“再包装一次模型调用”,而是把模型流、工具执行、消息队列和生命周期事件组织成可观察的运行。
agentLoop 是控制流 implementation
Section titled “agentLoop 是控制流 implementation”packages/agent/src/agent-loop.ts 负责:
- 把上下文交给
StreamFn; - 消费 assistant 增量事件;
- 将最终 assistant message 写入本次上下文;
- 校验并执行工具调用;
- 写入
ToolResultMessage; - 在安全点排出 steering 与 follow-up;
- 根据停止原因、工具结果和 hook 决定是否进入下一轮。
StreamFn 定义在 packages/agent/src/types.ts。它只要求输入 Model + Context + options,返回 AssistantMessageEventStream;core 因而不需要 import 某个具体 provider。
Agent 是有状态 facade
Section titled “Agent 是有状态 facade”packages/agent/src/agent.ts :: Agent 在 loop 外拥有:
- transcript 与当前 partial message;
- active run 和本次
AbortController; - steering/follow-up 队列;
- pending tool calls;
- 事件监听器及其 settlement。
它给上层提供 prompt、continue、steer、followUp、abort、subscribe 和 waitForIdle 等操作。调用者不必自己维护每个事件导致的状态变化,因此这个 facade 隐藏了真实复杂度。
core 的 interface 并不只是一组方法
Section titled “core 的 interface 并不只是一组方法”例如 Agent.subscribe 的 listener 可以异步返回,并且会被逐个 await。因此 agent_end 是最后一个事件,却不等于 waitForIdle() 已经完成。这个顺序约束也是 interface 的一部分;只看函数签名会漏掉它。
同理,AgentTool.execute 接收 AbortSignal 和 progress callback;executionMode 还会改变整批工具调用的串并行策略。这些都不是普通“函数回调”的无关细节。
第三层:pi-coding-agent 拥有 coding 产品策略
Section titled “第三层:pi-coding-agent 拥有 coding 产品策略”packages/coding-agent 把通用 Agent 变成可以操作代码仓库的产品。它拥有:
- read/bash/edit/write/grep/find/ls 等具体工具;
- 项目目录、设置、模型认证和资源发现;
- JSONL 会话、分支、压缩、重试和恢复策略;
- extension、interactive/print/RPC mode 与 TUI;
- 用户可见的 session 切换和生命周期。
这些概念若进入 pi-ai 或底层 agentLoop,provider adapter 和通用 runtime 就会被 coding 产品污染。
组装点不是业务层的反向依赖
Section titled “组装点不是业务层的反向依赖”packages/coding-agent/src/core/sdk.ts :: createAgentSession 创建或接收 ModelRuntime、SettingsManager、SessionManager 和 ResourceLoader,选择模型与工具,再构造 Agent 和 AgentSession。
新的组装路径还把生命周期拆成三步:
agent-session-services.ts :: createAgentSessionServices:创建与 cwd 绑定的模型、设置和资源服务;agent-session-services.ts :: createAgentSessionFromServices:基于已解析服务创建会话;agent-session-runtime.ts :: createAgentSessionRuntime:拥有会话替换、重绑与销毁流程。
这些函数位于上层 composition root。它们 import 下层并注入依赖,不需要让 pi-agent-core 反向 import coding-agent。
AgentSession 是产品 facade,也是复杂度汇聚点
Section titled “AgentSession 是产品 facade,也是复杂度汇聚点”packages/coding-agent/src/core/agent-session.ts :: AgentSession 订阅 AgentEvent,再协调会话持久化、extension 分发、自动重试、compaction、工具注册、模型切换和排队消息。
它对 interactive、print 和 RPC 提供了很高 leverage,但字段与职责也很多。应同时看到两面:
- 调用者获得一个统一产品入口;
- 多类产品变化可能汇聚到同一个 implementation,interface 和维护成本都在变宽。
“facade 很有用”和“facade 可能过宽”可以同时成立。
一条 prompt 的依赖、控制与事件方向
Section titled “一条 prompt 的依赖、控制与事件方向”下面的序列图描述典型 tool-call 路径,省略重试和 compaction 分支,但保留当前实现最关键的 event settlement 顺序。
sequenceDiagram
participant Host as UI / RPC / SDK host
participant Session as AgentSession
participant Agent as Agent
participant RuntimeLoop as agentLoop
participant Stream as StreamFn / provider adapter
participant Tool as AgentTool
Host->>Session: prompt(input)
Session->>Agent: prompt(messages)
Agent->>RuntimeLoop: run(context, config)
RuntimeLoop->>Stream: model + Context + options
Stream-->>RuntimeLoop: AssistantMessageEventStream
RuntimeLoop-->>Agent: message_end
Agent-->>Session: await AgentEvent listener
Session->>Session: extension message_end -> host listeners -> persist
Session-->>Agent: listener settled
RuntimeLoop->>Agent: beforeToolCall hook
Agent->>Session: extension tool_call
Session-->>Agent: allow / mutate / block
RuntimeLoop->>Tool: execute(validated args)
Tool-->>RuntimeLoop: progress / result
RuntimeLoop-->>Agent: tool_execution_* / turn_end
Agent-->>Session: await each AgentEvent listener
Session-->>Host: AgentSessionEvent
这里最容易混淆的是:
- package 依赖方向总体向下;
- 运行控制由上层通过函数、工具和 hook 注入下层;
- 运行结果再通过 event 向上返回;
Agent会等待AgentSession的异步 listener;但 listener 内部的message_endextension 先于 persistence,只有进入tool_calleffect 前可以确认触发它的 assistant message 已落盘;- 回调方向与编译依赖方向可以相反,并不形成循环 import。
这就是“下层不认识 UI,却仍能支撑 UI”的具体机制。
状态所有权:不要把所有 messages 都叫 session
Section titled “状态所有权:不要把所有 messages 都叫 session”这里的 owner 是指:在给定生命周期内,对状态承担权威写入与转换责任的运行时对象或 backend。interface 可以规定契约,缓存和 UI 可以保存 projection,但它们不因此成为事实源。一个概念在不同生命周期拥有多份状态并不矛盾;真正危险的是两处都自称同一份事实的权威写入者。
| 状态 | owner | 生命周期 | 一手来源 |
|---|---|---|---|
| provider 的增量事件与最终 assistant message | AssistantMessageEventStream |
一次模型请求 | packages/ai/src/utils/event-stream.ts |
| 本次 loop 的上下文副本与新消息 | runLoop |
一次 Agent 运行 | packages/agent/src/agent-loop.ts |
| transcript、active run、当前 model 引用、实际 steering/follow-up 队列、partial message | Agent |
一个进程内 Agent 实例 | packages/agent/src/agent.ts |
| model 目录、认证与 provider overlay | ModelRuntime |
一个 cwd-bound runtime | packages/coding-agent/src/core/model-runtime.ts |
| retry/compaction 状态、tool/extension registry、next-turn/custom/bash pending input | AgentSession |
一个 coding 产品会话实例 | packages/coding-agent/src/core/agent-session.ts |
| UI 可见的 steering/follow-up 文本列表 | AgentSession 保存 projection;投递真相仍在 Agent queue |
一个 coding 产品会话实例 | AgentSession._steeringMessages / _followUpMessages、Agent.steer / followUp |
| JSONL 会话树与历史 entry | SessionManager |
跨 CLI 运行的本地会话 | packages/coding-agent/src/core/session-manager.ts |
| entry/record/lane/fact | 具体 SessionStorage backend;SessionStorage / SessionRepo 只规定契约 |
该 backend 的持久化范围 | packages/agent/src/harness/session/types.ts、packages/agent/src/harness/session/memory.ts、packages/agent/src/harness/session/jsonl/storage.ts |
| 当前屏幕、选中项和渲染组件 | interactive mode / TUI | 一个宿主 UI 生命周期 | packages/coding-agent/src/modes/interactive/、packages/tui/ |
当两个 module 都保存“消息”时,要继续问:它是流式 partial、模型 transcript、产品历史、耐久 entry,还是 UI projection?名称相同不代表 owner 相同。
哪些 seam 是真的,哪些仍应保持怀疑
Section titled “哪些 seam 是真的,哪些仍应保持怀疑”| seam | interface | adapter / 调用者证据 | 判断 |
|---|---|---|---|
| 模型流 | packages/agent/src/types.ts :: StreamFn |
packages/ai/src/providers/、packages/ai/src/providers/faux.ts、packages/agent/test/agent-loop.test.ts |
真实 seam;隐藏 wire protocol,但 option 面仍较宽。 |
| 工具执行 | packages/agent/src/types.ts :: AgentTool |
packages/coding-agent/src/core/tools/、extension tools、packages/agent/test/agent-loop.test.ts |
真实 seam;schema 与 effect 相连,是安全策略点。 |
| 项目资源 | packages/coding-agent/src/core/resource-loader.ts :: ResourceLoader |
同文件的 DefaultResourceLoader、core/sdk.ts 注入、packages/coding-agent/test/utilities.ts 的测试 loader |
可替换 seam;产品概念留在 coding-agent。 |
| 会话存储 | packages/agent/src/harness/session/types.ts :: SessionStorage / SessionRepo |
session/memory.ts、session/jsonl/、packages/session-backends/sqlite-node/src/sqlite/repo.ts 与 harness session tests |
真实 seam;interface 同时承担一致性与耐久语义。 |
| 遥测 | packages/telemetry/src/index.ts :: TelemetryContext |
noop.ts、memory.ts、packages/telemetry/test/conformance.test.ts |
小 interface;适合测试和无遥测运行。 |
| 远程字节传输 | packages/client/src/transport.ts :: ByteTransportFactory |
packages/client/src/unix.ts 与 packages/client/test/connection.test.ts 的自定义 transport |
protocol/client 与具体 socket 分开。 |
| 远程 session 服务 | packages/server/src/types.ts :: PiServerService |
packages/server/src/testing/service.ts、packages/server/test/conformance.test.ts;server 本体只依赖该 interface |
seam 位置清楚;具体 durable runtime 由宿主提供。 |
| 产品扩展 | packages/coding-agent/src/core/extensions/types.ts :: ExtensionAPI 与 runner.ts :: ExtensionRunner |
packages/coding-agent/examples/extensions/ 与 extension tests |
能力真实,但 interface 很宽,不能称为“小而深”。 |
用 deletion test 检验 module 深度
Section titled “用 deletion test 检验 module 深度”想象删除某个 module:
- 删除 provider adapter,wire format、认证和流事件转换会扩散回每个调用者,说明它在隐藏复杂度;
- 删除
Agent,active run、队列、取消和事件归约会回到每个宿主,说明 facade 有 depth; - 删除
createAgentSessionServices,cwd 绑定的 model/settings/resources 组装会回到 main 与测试,说明它提供 locality; - 如果删除一个只转发参数的 wrapper,调用者几乎不增加知识,它可能只是浅 module。
这个测试比计算 package 数量或代码行数更有意义。
当前架构不是完成态
Section titled “当前架构不是完成态”静态图很容易把正在迁移的系统画得过于整齐。当前 HEAD 至少有以下张力。
1. coding-agent 仍直接依赖 core 和 pi-ai
Section titled “1. coding-agent 仍直接依赖 core 和 pi-ai”packages/coding-agent/package.json 同时依赖 pi-agent-core 与 pi-ai;core/sdk.ts 也直接使用 pi-ai/compat 的 model 和 stream。产品层没有只面对一个完全封闭的 runtime facade。
2. compat 仍带有全局 fallback
Section titled “2. compat 仍带有全局 fallback”core/sdk.ts 会调用 setDefaultStreamFn(streamSimple),为旧调用者保留全局默认值。显式注入有利于测试和局部推理,全局 fallback 有利于迁移兼容;两者并存意味着调用行为不能只从构造参数判断。
3. 根导出和 ExtensionAPI 很宽
Section titled “3. 根导出和 ExtensionAPI 很宽”packages/agent/src/index.ts 同时导出 Agent、loop、harness、session、compaction、tools、search 和 telemetry;coding-agent 根入口覆盖更多产品能力。公开面越宽,调用方便,但推荐路径、兼容成本和所有权越难仅靠 import 名称判断。
packages/coding-agent/src/core/extensions/types.ts :: ExtensionAPI 还覆盖工具、命令、快捷键、renderer、provider、session 和 UI 能力。它是有效产品 interface,却不是应该在 Go 中照搬成一个巨大 interface 的范例。
4. 两套 session/runtime 形态正在同仓库共存
Section titled “4. 两套 session/runtime 形态正在同仓库共存”coding-agent 的 AgentSession + SessionManager 已承担当前产品会话;agent-core 又公开了新的 SessionStorage、SessionRepo、lane、record 和 AgentHarness 方向。它们不能仅凭名字视为已经统一。
当前 packages/agent/src/harness/agent-harness.ts :: AgentHarness 的多数执行操作会返回 HarnessNotImplemented,对应 scaffold test 也明确验证这些边界。session storage、reducer、compaction 等子 module 有各自实现和测试,不代表完整 durable harness 已经可用。
5. interface 存在不等于端到端能力存在
Section titled “5. interface 存在不等于端到端能力存在”pi-server 提供 PiServerService 与 PiSessionRuntime interface,pi-protocol 提供 framing/codec/schema,pi-client 提供 transport-neutral client。这能证明远程 seam 的设计和部分 implementation,不能单凭类型文件证明某个企业级部署已经通过故障恢复、权限或多实例验收。
如何迁移到 Go,而不逐行翻译
Section titled “如何迁移到 Go,而不逐行翻译”| Pi 设计 | Go 中可以保留的原则 | 需要重新设计的地方 |
|---|---|---|
StreamFn |
用窄函数类型或小 interface 注入模型调用 | 选择 channel、iterator 或 callback,并定义关闭、错误和背压语义 |
| 判别联合事件 | 使用稳定 event kind 与明确 payload | Go 缺少 TypeScript closed union 的天然穷尽检查 |
AgentTool + TypeBox |
分开模型可见 schema 与宿主 effect | struct 校验、JSON Schema 和静态类型不会自动来自同一机制 |
AbortSignal |
用 context.Context 贯穿 provider、tool 和等待路径 |
context 取消同样不是副作用回滚,goroutine 退出仍需实现保证 |
Agent.subscribe |
对外只暴露领域事件,不 import UI | 必须决定同步回调、channel 缓冲、慢消费者和 panic 隔离 |
SessionRepo |
让 memory/SQLite/远端存储在同一 seam 下做契约测试 | 明确事务、单写者、锁、跨进程一致性和迁移策略 |
| composition root | 在 main/factory 集中选择 adapter 并显式注入 | 不要把所有依赖塞进一个全局 service locator |
ExtensionAPI |
按能力拆成工具、事件、UI、provider 等较小 interface | Go 的巨大 interface 会让每个 adapter 难以实现和测试 |
迁移前先写出 interface 的行为契约:谁关闭 channel、错误作为值还是返回、取消后是否仍会有最终事件、工具 effect 是否可重放。仅翻译类型名不会得到等价架构。
五个源码练习
Section titled “五个源码练习”练习 1:从 manifest 重画依赖图
Section titled “练习 1:从 manifest 重画依赖图”只读各 package 的 package.json,区分直接依赖与运行时回调。然后解释为什么 pi-server 可以声明 PiServerService 而不直接依赖 pi-agent-core。
练习 2:追一条 prompt
Section titled “练习 2:追一条 prompt”从 AgentSession.prompt 进入 Agent.prompt 和 runLoop,记录每次跨 seam 的输入、输出、错误和取消方式。不要跳进所有实现细节。
练习 3:为每份状态指定 owner
Section titled “练习 3:为每份状态指定 owner”挑选 messages、partial assistant、tool progress、retry attempt、session entry 和 UI component,写出唯一 owner 与生命周期。若同一份数据被复制,说明哪一份是事实源、哪一份是 projection。
练习 4:做 adapter 证据表
Section titled “练习 4:做 adapter 证据表”对 StreamFn、SessionStorage、TelemetryContext 和 ByteTransportFactory 分别找至少两个 adapter 或“生产 + 测试”实现。找不到第二个时,把它标成假设 seam,而不是成熟扩展点。
练习 5:执行 deletion test
Section titled “练习 5:执行 deletion test”假想删除 Agent、AgentSession 或 createAgentSessionServices。列出会扩散到调用者的复杂度,再判断这个 module 提供的是 depth、单纯便利,还是重复抽象。
读完后应能独立回答:
pi-ai、pi-agent-core和pi-coding-agent各自不应该知道什么?- package 依赖向下时,工具和 hook 为什么仍能让上层控制下层行为?
agentLoop与Agent的 interface 分别是什么,为什么不能合并理解?AgentSession提供了什么 leverage,又承担了什么宽度成本?- 至少指出四份不同生命周期的“消息状态”及其 owner。
- 为什么
SessionStorage的多个 adapter 与 conformance test 比一个 TypeScript interface 更能证明 seam 存在? ExtensionAPI很有用,为什么仍不能把它评价为小而深?- 当前
AgentHarness哪些部分可以作为源码事实,哪些只能视为目标 interface 或脚手架? - 远程 protocol/client/server 的存在为什么不等于企业生产 Agent 已经完成?
- 把其中一个 seam 迁移到 Go 时,运行时契约会在哪些地方变化?