Pi 如何演变成今天的架构
这不是一篇“如何阅读 Git 历史”的教程,而是一篇已经替你读过历史之后的工程复盘:Pi 最初解决什么问题,在哪些约束下抽出新边界,这些边界后来如何被测试、修复、回退,最终为什么形成今天的 module 组合。
证据快照:Pi
ccb9464114c2542aad444054f590f06b03a324ac(2026-08-28)。正文只使用该提交祖先链中的 Git 对象、已提交源码、测试与仓库文档;未提交工作树不作为证据。
本文按时间讲全景。架构演进专题继续深挖 Agent Loop、工具、compaction 与 durable session 的状态语义;当前架构回答今天各 module、interface 与状态 owner 是什么。
如果把本文用于一次技术分享,可以先读全景、每阶段的“代码如何变好”和最后五条结论;02B 是用来追问调用顺序与失败语义的证据层,03 则只描述当前静态结构。
文中四种判断不会混写:
- 事实:可以在指定 commit 的路径、symbol、diff 或测试断言中直接核对。
- 有依据的推断:多条事实支持的一种工程解释,但不是作者心理活动的证明。
- 设计经验:面向自研 Agent 的迁移结论。
- 不能推出:证据尚不足以支持的更强结论。
本文把 module 理解为隐藏一组实现知识的代码单元,把 seam 理解为可替换行为的接入点,把 owner 理解为某份状态的权威写入者;effect boundary 是调用外部工具或提交 checkpoint 前后不能混淆的边界。depth、leverage 与 locality 分别用来追问 module 隐藏多少复杂度、被多少调用者复用,以及变化能否留在局部。完整术语定义见当前架构。
先看全景:Pi 不是“拆了一次包”,而是连续收紧边界
Section titled “先看全景:Pi 不是“拆了一次包”,而是连续收紧边界”flowchart TD
A[2025-08 产品闭环] --> B[2025-08~09 provider 数据协议]
B --> C[2025-10~2026-01 runtime 与产品分层]
C --> D[2025-11~2026-04 session 与 effect 语义]
D --> E[2026-04~06 收窄仓库与兼容面]
E --> F[2026-07 ModelRuntime 与质量反馈]
F --> G[2026-07~08 durable 与 remote seam]
这些阶段并非互斥分支:session、公开 API 和新 runtime 基础设施曾并行推进,因此日期会交叠;箭头表示主叙事顺序,不表示单一因果链。
这条主线可以概括为七步:
- 先把 OpenAI、工具、session、CLI/TUI 组在一起,完成一个能用的 coding Agent。
- provider 增多后,先抽消息、模型、流和工具 schema,而不是先造一个万能 Agent framework。
- 再把通用 loop 与 coding 产品拆开;第一次 transport 方案并非终点,后来收敛为更窄的
StreamFn。 - session、compaction、extensions 和工具调度逐渐从“功能”变成有顺序约束的状态机语义。
- 删除旧实现和旁支产品,同时用 compat 与 revert 控制公开 API 收窄的代价。
- 将模型、鉴权和 provider overlay 收到
ModelRuntime,并新增 eval harness,让质量反馈不只依靠单元测试。 - 最后才出现独立的 durable storage、wire protocol、client、server 与 telemetry seam;这些模块扩展了运行形态,但并不等于完整 durable Agent 已经完成。
第一阶段:先完成产品闭环,而不是先设计 framework(2025-08)
Section titled “第一阶段:先完成产品闭环,而不是先设计 framework(2025-08)”a74c5da11:最初已经有 Agent 包,但它是“整个产品”
Section titled “a74c5da11:最初已经有 Agent 包,但它是“整个产品””事实。 根提交 a74c5da11 已包含 @mariozechner/pi-agent,不能写成“Pi 最初没有 Agent 包”。它的 packages/agent/src/agent.ts 同时:
- import
OpenAI并在Agentconstructor 中创建 client; - 定义
callModelResponsesApi()与callModelChatCompletionsApi(); - 调用
executeTool(); - 持有可选的
SessionManager; - 发出
AgentEvent给 renderer。
同一 package 还拥有 src/cli.ts、src/session-manager.ts、src/tools/tools.ts 和三套 renderer;其 package.json 直接依赖 openai 与 pi-tui。pi-tui 从根提交起就是独立 workspace,但 pi-agent 同时知道 provider、产品会话和 UI 适配。
调用链近似为:
CLI -> Agent -> OpenAI API -> executeTool -> SessionManager / renderer代码为什么在这个阶段是合理的。 首个版本把一次 prompt 的完整反馈环路放在局部:模型返回 tool call 后,代码可以直接执行、渲染并写入 session。它牺牲可替换性,换来低协调成本和可运行产品。历史没有证明作者曾先设计通用 runtime 再回填产品。
暴露出的约束。 一旦需要 Anthropic、Gemini、浏览器宿主或不同 session 策略,调用方必须同时理解 OpenAI wire format、工具执行和产品状态。这些变化轴并不相同,混在一个 Agent 中会共同变化。
给自研 Agent 的启示。 第一版应先闭合 model → tool → model → user 的链路,暂时共置不是失败。抽象时机应由已经出现的变化轴驱动,而不是按“标准三层架构”预先分包。
Go 对照与失效处。 可以先做一个拥有 provider client、tool registry 和 session store 的 Agent struct;当第二个 provider 或第二种宿主真正出现,再拆 interface。TypeScript 的 ESM package 与 Go package 的编译边界不同,不能用目录数量直接比较耦合程度。
第二阶段:先把 provider 差异压成数据协议(2025-08 至 09)
Section titled “第二阶段:先把 provider 差异压成数据协议(2025-08 至 09)”f064ea0e1 只是脚手架,不是迁移完成
Section titled “f064ea0e1 只是脚手架,不是迁移完成”事实。 f064ea0e1 新建 packages/ai 及多家 provider SDK 依赖,但当时 packages/ai/src/index.ts 只导出 version 和 TODO。该提交没有修改旧 packages/agent;后者仍直接依赖和 import OpenAI。
不能推出。 提交标题写着 unified AI package,不等于统一 API 当天已经承接旧 Agent。把标题倒读成完整迁移,会抹掉后续真正形成协议的提交。
004de3c9d:stream 成为 provider-neutral contract
Section titled “004de3c9d:stream 成为 provider-neutral contract”事实。 004de3c9d 在 packages/ai/src/types.ts 定义:
Model、Context、Message与ToolCall;- 判别联合
AssistantMessageEvent; GenerateStream extends AsyncIterable<AssistantMessageEvent>;GenerateFunction(model, context, options)。
packages/ai/src/generate.ts :: QueuedGenerateStream 把 provider callback 转成可迭代事件流,并保留 finalMessage()。同提交的 packages/ai/test/generate.test.ts 对 Anthropic 与 OpenAI 分别覆盖 basic text、tool calling、streaming;这比一个共同 interface 更能证明不同 adapter 正在共享同一协议。
代码如何变好。 renderer 或 Agent 不再需要分别消费 OpenAI/Anthropic 的 chunk shape;它们只处理 start、delta、done/error 和最终 AssistantMessage。变化频繁的 wire protocol 被 adapter 吞掉,稳定的数据协议留给上层。
5f7a50dee 与 e8370436d:可注入模型流,schema 同时服务静态与运行时
Section titled “5f7a50dee 与 e8370436d:可注入模型流,schema 同时服务静态与运行时”事实。 5f7a50dee 给当时位于 packages/ai/src/agent/agent.ts :: prompt() 的 loop 增加可选 streamFn:默认仍为 streamSimple,测试或其他宿主可注入替代实现。这是后来 StreamFn seam 的早期形态,但此时 loop 仍在 pi-ai。
e8370436d 将工具参数从 Zod 迁移到 TypeBox;packages/ai/src/validation.ts :: validateToolArguments() 用 AJV 在执行前校验,packages/ai/test/tool-validation.test.ts 固定了正确输入、缺字段、格式错误和类型安全 execute 的行为。provider adapter 也同步消费相同 schema。
有依据的推断。 这些 diff 支持“pi-ai 首先抽象的是跨 provider 数据与流协议”:模型调用的变化被压在 GenerateFunction/provider adapter 后,工具输入则在运行时 schema 处统一。它们不支持“此时 pi-ai 已是纯 provider 层”——Agent loop 当时也在这个包中。
设计经验。 provider abstraction 的价值不只是统一一个 Generate() 方法,而是统一消息、增量事件、停止原因、usage 与 tool schema。只抽一个客户端 interface,却让上层继续 switch provider-specific payload,等于没有真正建立边界。
Go 对照与失效处。 Go 可以用 ModelClient.Stream(ctx, request) (<-chan Event, error) 或 iterator-like API,并让所有 adapter 通过 contract tests。TypeBox 能从 schema 推导 TypeScript 类型;Go struct、JSON Schema 和运行时 validation 通常需要明确选择事实源,不能假定它们自动一致。
第三阶段:runtime 与 coding 产品分开,但第一次 seam 并不稳定(2025-10 至 2026-01)
Section titled “第三阶段:runtime 与 coding 产品分开,但第一次 seam 并不稳定(2025-10 至 2026-01)”ffc9be886:真正的结构转折是复制旧闭环,再建立新路径
Section titled “ffc9be886:真正的结构转折是复制旧闭环,再建立新路径”事实。 ffc9be886 的前后树同时发生三件事:
- 原
packages/agent的 CLI、renderer、session、tools 与旧Agent被保留到packages/agent-old; - 新
packages/agent定义AgentState、Agent、AgentTransport、ProviderTransport、AppTransport与单元/E2E 测试; - 新
packages/coding-agent接管 CLI、read/bash/edit/write 工具和SessionManager,其src/main.ts实际构造new Agent(...)。
新 core 的 packages/agent/test/agent.test.ts 覆盖 state 初值、subscription、mutator、message queue 与 abort controller。它说明新的 Agent 已成为有状态 facade,而不只是目录占位。
但是 packages/agent/src/transports/ProviderTransport.ts 仍从 pi-ai import agentLoop。当时的依赖含义是:generic Agent 管状态,transport 选择执行路径,而 provider 包仍拥有 loop。
不能推出。 ffc9be886 的标题带 WIP;新目录和测试证明过渡结构存在,不证明职责已经稳定。92bad8619 到 2025-11-10 才删除 agent-old 的 4,143 行历史实现,而且同一 diff 将 packages/pods/src/commands/prompt.ts 对旧包的调用改成直接抛出 Not implemented。所以它能证明旧实现被收口,也同时留下未迁移消费者/功能退化的证据,不能简化成“所有调用者迁移完成”。
a055fd448 → 3caf35940:用窄函数替换较宽 transport abstraction
Section titled “a055fd448 → 3caf35940:用窄函数替换较宽 transport abstraction”事实。 a055fd448 明确自称 not compiling。它删除 AgentTransport/ProviderTransport/AppTransport,把 agentLoop 从 packages/ai/src/agent/ 移到 packages/agent/src/agent-loop.ts,并让 Agent 接受 streamFn。随后 6ddc7418d、e49e78732、92898f486、4d1936d3d、41af99ccc 分别修正 WIP、streaming state、异步 streamFn、默认 stream 和多消息 prompt。
到 3caf35940 的 v0.31.0 发布树,packages/agent/CHANGELOG.md 才把公开迁移面记录为:
- 移除三种 transport;
agentLoop/agentLoopContinue归入 agent package;- 引入
AgentLoopConfig、AgentContext、AgentTool与StreamFn。
同一发布树的 packages/agent/test/agent-loop.test.ts 直接注入 mock stream,覆盖 transformContext、custom message conversion、tool call/result、continue 与 queue event 顺序。真实测试调用者说明 StreamFn 不只是类型装饰。
结构由:
Agent -> AgentTransport -> ProviderTransport -> pi-ai agentLoop -> provider收敛为:
Agent -> agentLoop -> injected StreamFn -> pi-ai provider adapter -> injected AgentTool代码如何变好。 Agent control flow 与 provider wire protocol 的所有权变清楚:pi-agent-core 决定 turn、tool 和 queue 的顺序,pi-ai 提供 model/message/stream 与 adapter。测试只需注入一个函数,不必构造有多种职责的 transport object。
代价。 StreamFn 的 options 和事件契约仍然跨 package;coding-agent 同时依赖 core 与 pi-ai。后来为兼容旧 extension,又出现 default stream fallback,因此“显式注入”并未在所有路径上绝对化。
不要混淆命名与架构。 e1856daf5 的 pi-agent → pi-agent-core 是 npm deprecation 触发的发布名变更;507639c76 只修正文档名称。它们不是上述 loop 搬迁。
设计经验。 loop 应依赖“如何取得一次模型事件流”,而不是依赖某个 provider client 或包揽 host transport。对 Go 而言,一个窄 StreamFunc 有时比包含认证、连接、状态和调用的宽 interface 更容易测试;但若项目有多个实现且需要生命周期方法,小 interface 仍可能优于函数类型。
第四阶段:session 从日志文件变成产品状态机(2025-11 至 2026-01)
Section titled “第四阶段:session 从日志文件变成产品状态机(2025-11 至 2026-01)”5e988b444 → 812f2f43c:先区分事件与事实,再决定何时落盘
Section titled “5e988b444 → 812f2f43c:先区分事件与事实,再决定何时落盘”这两个提交常被写反;祖先关系和时间均表明 5e988b444 在前。
事实。 5e988b444 删除 SessionEventEntry、saveEvent() 以及 main.ts 对每个 Agent event 的持久化,只保留 message、model change 与 thinking-level change。十分钟后的 812f2f43c 再增加 sessionInitialized、pendingMessages 与 shouldInitializeSession():只有出现至少一条 user 和一条 assistant message 后才创建新 session 文件,continue/resume 则沿用既有 session。
这两个提交没有新增对应行为测试,证据强度来自父子源码 diff,而不是测试。因此可以确认数据模型和创建时机发生变化,不能声称所有恢复边界当时已经被自动化验证。
代码如何变好。 观察事件不再自动等于 durable fact;空启动也不会制造空 session。恢复所需数据与 UI/streaming 暂态被分开。
设计经验。 自研 Agent 应先列出恢复所需的最小事实,再决定日志格式。token delta、tool progress 适合 observability,不一定适合业务恢复。若确实采用 event sourcing,则要承诺 schema version、幂等 replay 与 projection,而不是因为“已经有事件”就全量落盘。
6c2360af2 → c89b1ec3c → a38e61909 → 5a9d844f9:compaction 从功能变成受控状态转换
Section titled “6c2360af2 → c89b1ec3c → a38e61909 → 5a9d844f9:compaction 从功能变成受控状态转换”事实。 6c2360af2 先新增 packages/coding-agent/src/compaction.ts、test/compaction.test.ts 和 fixture;确定性测试覆盖 cut point、单次/多次 compaction 与 model/thinking state,另有需 Anthropic token 才运行的 summarization test。c89b1ec3c 再将它接入 /compact、/autocompact、自动触发、TUI 和 HTML;该提交本身没有修改 compaction 测试。
a38e61909 随后在 core/agent-session.ts 加入 overflow recovery、split-turn summary、mid-turn proactive abort 与文本重放重试。5a9d844f9 修正的正是这套设计:它移除 proactive abort,新增 Agent.continue() / agentLoopContinue(),让 overflow compact 后从已有 context 继续;AgentSession 同时区分 overflow 自动重试与 threshold 非自动重试,并在 compaction 中阻止新 prompt。
有依据的推断。 测试与控制流支持把 compaction 理解为 checkpoint:它改变以后送给模型的上下文,因此必须在完整 turn 边界上完成,而不是任意中断当前 effect 后“压一下文本”。更细的失败与回退链见架构演进专题。
d0a4c3702 与 c6fc08453:控制输入和扩展能力都获得明确入口
Section titled “d0a4c3702 与 c6fc08453:控制输入和扩展能力都获得明确入口”事实。 d0a4c3702 将一个 queueMessage() 拆成 steer() 与 followUp(),并在 AgentLoopConfig 中拆出两个 getter。前者在当前 run 的安全点进入,后者在 run 结束后发起下一轮。
扩展侧则先有 04d59f31e 的 hooks、e7097d911 的 custom tools,最后由 c6fc08453 合成 ExtensionAPI、ExtensionRunner 与统一 discovery;该提交还迁移 settings、CLI flag、session version 和历史 hookMessage。extensions-runner.test.ts 覆盖多 extension 的 tool/command 收集、冲突诊断和 handler error listener。
代码如何变好。 queue 的调度语义从一个模糊容器变成两个调用者必须选择的 API;扩展也不再分散在“hook 系统”和“custom tool loader”两套生命周期中。
代价与不可照搬处。 当前 ExtensionAPI 覆盖工具、session、UI、provider、command 等很多能力,是有效产品接口,但不是小 interface。Go 中更适合按 capability 拆成若干窄接口;否则每个 extension adapter 都被迫知道过多宿主能力。
第五阶段:把工具副作用、扩展时序和 composition root 写进测试(2026-03 至 05)
Section titled “第五阶段:把工具副作用、扩展时序和 composition root 写进测试(2026-03 至 05)”这一阶段最重要的变化不是新 package,而是先前抽象开始被并发、重入和副作用击穿,随后由修复测试收紧。
工具调用不再只是 execute(args)
Section titled “工具调用不再只是 execute(args)”| 提交 | 历史源码与测试固定的行为 | 对代码质量的具体改善 |
|---|---|---|
63ac2df24 |
AgentSession._installAgentToolHooks() 将 tool_call/tool_result interception 从 wrapper 移到 Agent hook;调用前等待 _agentEventQueue。agent-session-concurrent.test.ts 断言 extension 观察 tool call 时,user 与 assistant 已按序持久化。 |
extension 看见一致的 session projection,避免“工具已开始,但触发它的 assistant message 还没落盘”。 |
208a2cc12 |
agent-loop.ts 删除执行一个工具后立即检查 steering、跳过剩余工具的路径;测试 should inject queued messages after all tool calls complete 断言两个工具都执行后才注入消息。 |
steering 不再主动制造“只执行已接受 batch 前半段”的路径;这不保证工具都成功,也不提供事务原子性。 |
b5f425ad1 |
AgentTool.prepareArguments 在 schema validation 前转换旧参数;agent-loop 测试验证准备后的参数才参与校验,coding-agent 回归测试覆盖 edit 旧输入迁移。 |
兼容转换局限在 effect 前的 adapter,不污染 canonical tool schema。 |
bfa11a50e |
AgentTool.executionMode 可以把整个 batch 强制为 sequential;测试用 slow/fast tools 明确断言 parallel 与 sequential 的启动顺序。 |
并发要求成为 capability contract,而不是散落在调用者的 await 顺序中。 |
32bcdc973 |
AgentSession._handleAgentEvent() 改为可被 Agent 直接等待的 async listener,删除 _agentEventQueue;回归测试继续固定慢 handler 下的消息顺序和 tool effect 前的持久化状态。 |
把一次局部 barrier 提升为 listener settlement 契约,同时保留“extension message_end 先于 persistence”的精确阶段差异。 |
设计经验。 Tool 是 effect boundary,至少需要同时考虑 runtime schema、兼容转换、权限/extension interception、取消、并发模式和 stop reason。把它定义成一个普通 callback,只解决了“怎么调用”,没有解决“何时可以安全调用”。
Go 对照与失效处。 context.Context + Tool.Execute() 是起点,但 context 取消不会回滚已经写文件或发出的请求。若一批 tool call 需要 all-or-nothing,必须另建事务/补偿语义;不能把 Pi 的“完成整个 batch 再 steering”误读为原子事务。
d86122cbd → 9f9277ccd:产品组装从一个大对象里抽出,再继续收窄
Section titled “d86122cbd → 9f9277ccd:产品组装从一个大对象里抽出,再继续收窄”事实。 d86122cbd 新增 agent-session-runtime.ts :: AgentSessionRuntimeHost,把 new/resume/fork/import 时的 session replacement、cwd 切换、extension shutdown 与依赖重建从 AgentSession 和各 mode 中抽出。agent-session-runtime-events.test.ts 覆盖 switch/fork 的 lifecycle event。
三天后的 9f9277ccd 又用 closure-based AgentSessionRuntime / CreateAgentSessionRuntimeFactory 取代 Host/bootstrap class,并新增 agent-session-services.ts:process-global 固定输入被 factory 闭包持有,SettingsManager、resource loader 等 cwd-bound service 在目标 cwd 重新创建。对应 suite 测试固定了 switch cancellation、跨 cwd replacement、目标 session 的 model/thinking 恢复以及 stale resource settings 回归。
代码如何变好。 AgentSession 不再同时拥有“一轮 Agent 产品策略”和“替换整个会话 runtime”的职责;composition root 也不在 parsing/creation 深处直接 log 或 exit。第二次重构说明“抽出一个 class”本身不是终点:真正稳定的边界是哪些依赖跨 session 复用,哪些必须随 cwd 重建。
真实权衡。 d86122cbd 的 replace() 先成功创建 next runtime,再 shutdown/dispose old runtime;9f9277ccd 改为 teardown old → create next → apply next,并让创建错误直接传播。后者换来依赖按目标 cwd 重新解析、切换过程更集中,却不再保证创建失败时旧 runtime 仍可继续使用。不能把这次重构单向写成“更安全”。
设计经验。 在 Go 中可让 RuntimeFactory 闭包持有进程级依赖,返回拥有 cwd/session 生命周期的 runtime。切换时应根据新旧资源能否并存、创建失败后是否要保留旧 runtime,选择 create-next → teardown-old 或 teardown-old → create-next,并用失败测试锁定承诺。不要用全局 service locator 隐藏两类生命周期。
第六阶段:删除也是架构演进,compat 与 revert 也是反馈(2026-04 至 06)
Section titled “第六阶段:删除也是架构演进,compat 与 revert 也是反馈(2026-04 至 06)”清理过渡物更早已经开始:92bad8619 删除 agent-old,0f98decf6 删除独立 proxy package。到 2026 年 4~5 月,仓库又移除两个旁支产品和 web-ui:
| 提交 | 可核对的删除 | 能确认与不能确认 |
|---|---|---|
0ed0d4343 |
删除 mom 与 pods,约 12,964 行。 |
能确认 monorepo 去掉 Slack/sandbox Agent 和远端 model-pod 产品;不能从提交标题推出完整产品战略。 |
b141e1fa2 |
删除 web-ui workspace,约 18,564 行,并移除相关依赖。 |
能确认当前仓库不再维护该浏览器 UI 产品;不能推出 core 从此不需要 browser compatibility。 |
这些删除直接缩小了 build/test/release surface,使后来的 workspace 更集中在 AI protocol、Agent runtime、coding product、TUI 和远程/durable infrastructure。这里的“更集中”是树结构结果,不是作者动机的引述。
公开 API 变窄时,兼容层不能假装不存在
Section titled “公开 API 变窄时,兼容层不能假装不存在”事实。 8a0903ebf 将 pi-ai 根 barrel 收窄为 core-only、减少 import side effect,并新增 pi-ai/compat 保存旧 global API;lazy-module-load.test.ts 断言 import 根入口不会加载 provider SDK,实际调用 Anthropic 时只加载对应 SDK。coding-agent extension loader 还把旧 root specifier 映射到 compat。
后续历史同时包含两组反例:
0d89a3337增加 selective base entrypoints,717a8f958四天后把它整体回退;1235c0ec6要求 agent stream 显式脱离 compat,b9e5c5d94次日恢复 legacy extension 省略streamFn时的 default fallback,并增加对应测试。
代码如何变好。 core import 变得更可 tree-shake、provider SDK 延迟加载;兼容路径被单独命名,而不是继续污染推荐入口。付出的代价是两套路由和一个全局 default stream,调用行为不能只看 constructor 参数。
设计经验。 API 收窄必须同时设计迁移入口、淘汰窗口和真实 consumer test。Go module 也会遇到相同问题,但没有 ESM side effect/tree-shaking 这一维;不要机械复制 compat package,先确认你的兼容风险来自源码 API、配置、持久化数据还是动态插件。
第七阶段:从本地产品走向可组合 runtime 基础设施(2026-05 至 08)
Section titled “第七阶段:从本地产品走向可组合 runtime 基础设施(2026-05 至 08)”a5b27367d:新的 AgentHarness 方向从 core 内部开始,但没有一次完成
Section titled “a5b27367d:新的 AgentHarness 方向从 core 内部开始,但没有一次完成”事实。 a5b27367d 在 packages/agent/src/harness/ 一次加入 AgentHarness、execution environment、session tree、compaction 与对应测试。它尝试把 coding-agent 中可复用的执行/会话概念下沉到 core。
后续并不是线性“完成”:接口和 session model 多次重写。到 1d0c97471,新的 SessionStorage / SessionRepo、record/lane 与 830 行 backend conformance suite 先在 memory backend 落地;44289550a 虽题为 promote durable harness API,但其 agent-harness-scaffold.test.ts 直接断言 prompt() 抛 HarnessNotImplemented、close() 后再 prompt() 抛 HarnessClosed。同一快照的 agent-harness.ts :: unavailable() 还被 compact()、resume()、createLane() 等调用;而 watch()、lane()、lanes() 已有 scaffold 返回路径,不能一概写成抛错。
不能推出。 已有深厚的 storage/session contract,不等于 durable Agent runtime 已能在任意中断点恢复执行。
351efc828 与 97fa14e39:失败不能留下“看似成功”的 effect 或 checkpoint
Section titled “351efc828 与 97fa14e39:失败不能留下“看似成功”的 effect 或 checkpoint”事实。 351efc828 在 agent-loop.ts 检查 assistantMessage.stopReason === "length":即使截断 JSON 能被解析,也不执行其中任何 tool call;测试断言 side-effect 数组为空,并生成 error tool result 让模型可重发。
97fa14e39 对 compaction 做同类收紧:摘要若因 length 截断即失败,回归测试断言没有写入 compaction entry。
设计经验。 Agent 安全边界不能只问“payload 能否 parse”,还要问产生 payload 的过程是否完整。对外部 effect 和未来 context checkpoint,都应采用 fail closed;错误路径还要断言 durable state 未改变。
9993c9690:ModelRuntime 收拢模型、鉴权与 provider overlay
Section titled “9993c9690:ModelRuntime 收拢模型、鉴权与 provider overlay”事实。 9993c9690 用 packages/coding-agent/src/core/model-runtime.ts :: ModelRuntime 替换内部 ModelRegistry:agent-session-services.ts 创建 runtime,AgentSession 通过它完成 getAuth()、checkAuth()、model lookup 和 extension provider registration;旧 ModelRegistry 保留为 extension compatibility facade。新增测试覆盖 auth options、Cloudflare compatibility、runtime credentials 与 model resolution。
代码如何变好。 “模型目录”和“拿到可执行请求所需认证”不再由多个 manager 分别解释;cwd-bound composition root 只注入一个 canonical runtime。代价是这个 facade 本身较深,且 extension 兼容层继续存在。
storage、eval、protocol、client、server、telemetry 依次形成独立 seam
Section titled “storage、eval、protocol、client、server、telemetry 依次形成独立 seam”| 历史节点 | 历史源码与测试证据 | 抽象出来的约束 |
|---|---|---|
9e7582aa0(07-21) |
新增 SQLite session backend、migration 与 storage tests;断言 active leaf、session summary 等 materialized state 事务化更新,失败后恢复内存 cache。 | durable state 不是把 JSONL 换成 SQL,而是定义 append、fork、materialization 与失败原子性;该 adapter 并非 coding-agent 当前默认存储。 |
eafe11fb9(07-25) |
新增 private packages/evals 与 pi-harness.ts,把 coding Agent 包装为 Vitest eval target,转发 assistant/tool 事件并统一 cleanup;真实 .eval.ts 运行要求 provider/model。 |
质量反馈面从确定性单元测试扩展到模型行为 eval;历史代码不证明实际得分或 Agent 质量已经提高。 |
56eb685b6(07-30) |
新增 TypeBox command/event schema、CBOR codec 和 framing;protocol tests 覆盖 fragmented frames、invalid data、frame limit 与 request/response 类型。 | wire contract 从某个 RPC mode 中独立,序列化与 runtime schema 成为边界。 |
33bc0a7b8(07-31) |
pi-client 只依赖 pi-protocol;ByteTransportFactory 隐藏 socket,state tests 固定 disconnect、stale response 与 snapshot revision 语义。 |
client lifecycle 与具体传输分离,远程调用者不必 import Agent runtime。 |
2e60d3cbd(07-31) |
PiServerService / 当时的 PiSessionBackend 隐藏 durable session implementation,listener 隐藏 Unix transport;conformance tests 覆盖 handshake、auth、乱序 response、terminal error 和 graceful close。 |
server 负责协议与 acquisition,具体 session runtime 由宿主实现。 |
1d0c97471、a838c069e、4a0e2f115(08-04~07) |
memory conformance 后,JSONL 使用 staging + atomic rename 发布 fork/repair,串行化写入,并只修复 torn tail;测试注入 rename/staging failure 和 corruption。 | adapter 必须共享失败语义,interface 本身不足以证明 seam 成立。 |
6b461b75b、35f5c265d(08-05~06) |
前者把 telemetry contract 与 noop implementation 从 pi-ai/agent 内部抽成 packages/telemetry;后者增加 memory reference adapter 和 conformance suite,固定 callback settlement、同步/异步 rejection 与 snapshot 隔离。 |
observability 是跨 provider/runtime 的横切协议,不应由某一 provider 包独占;adapter 也需要共享行为测试。 |
有依据的推断。 这些提交共同表明 Pi 正从“一个本地 CLI 产品”扩展出可远程访问、可替换持久化、可观测和可评测的 runtime 基础设施。这个解释由独立 package、adapter 和 conformance tests 支持。
不能推出。 protocol/client/server 和 durable records 的存在,不证明完整的远程生产部署、权限模型或 crash-safe tool effect 已经完成。当前仓库里的 PiServerService adapter 只有 src/testing/TestServerService;RemoteSession 虽有测试并从 coding-agent 导出,主 CLI/TUI 没有 production caller。SQLite repository 通过 backend conformance、lease/crash/search 测试,却不是 coding-agent 默认 session store。telemetry 也只有 noop/memory reference adapter,尚无 vendor exporter。再加上 AgentHarness 的显式 scaffold 边界,更准确的结论是“基础 seam 已形成并被局部验证”,不是“远程 durable Agent 已交付”。
最终为什么是今天这组 module
Section titled “最终为什么是今天这组 module”当前根 workspace 包含 ai、agent、coding-agent、tui、protocol、client、server、session-backends、telemetry 与 evals。
工程解释(推断)。 从可见提交的形成顺序看,更稳妥的解释是:这些 module 分别围绕 provider、控制流、产品策略、远程访问、持久化和质量反馈等变化轴逐步出现,而不是一次拆包完成。仓库历史无法证明作者从未有过未提交的总体蓝图。
| 当前 module | 历史上被抽离的变化轴 | 今天主要隐藏什么 | 仍存在的张力 |
|---|---|---|---|
pi-ai |
provider、模型、消息、stream、tool schema | wire format、provider SDK 与模型协议 | compat/global registration 与多入口成本 |
pi-agent-core |
turn/tool/queue 的控制流 | Agent state、loop、事件与 tool effect 调度 | public surface 较宽;durable harness 尚未完整接通 |
pi-coding-agent |
coding 工具、session、compaction、extension、CLI/TUI 策略 | 产品 lifecycle 与 composition root | AgentSession 和 ExtensionAPI 仍很宽;直接依赖 core 与 pi-ai |
pi-tui |
terminal 输入与渲染 | viewport、editor、component rendering | 它是宿主 UI primitive,不是 Agent runtime |
pi-protocol / pi-client / pi-server |
远程 session 的 wire、client lifecycle 与 service acquisition | 序列化、连接状态、远端 session facade | seam 已有测试,不等于生产控制面完整 |
pi-session-backend-* |
持久化实现差异 | SQLite/JSONL 等一致性细节 | 恢复 tool effect 仍需更高层协议 |
pi-telemetry / pi-evals |
观察与质量反馈 | span contract、模型行为评测接入 | telemetry、eval 与 correctness test 不能互相替代 |
依赖方向也由历史解释:coding product 依赖 Agent control flow,Agent 依赖 AI data/stream contract;远程 client 只依赖 protocol,server 通过 service interface 获得 session runtime。控制流和事件回调可以反向返回,但不要求底层 package import 上层产品。
不按 package 数量评价“解耦”,而看 module depth
Section titled “不按 package 数量评价“解耦”,而看 module depth”这里的 interface 不只是 TypeScript 类型,还包括调用顺序、错误、取消和持久化约束。沿历史做 deletion test,可以得到更谨慎的判断:
- 删除
StreamFnseam,provider 调用与 mock/faux stream 的知识会扩散回 Agent Loop 和每个宿主;真实 provider path 与测试 adapter 都穿过它,因此它提供了 leverage 和 locality。 - 删除
SessionStorage/SessionReposeam,append、fork、lane、materialization 与 failure semantics 会重新散落到 memory、JSONL 和 SQLite 调用者;多个 adapter 共享 conformance suite,说明这不是只有一个实现的假想 seam。 - 删除
ModelRuntime,模型 overlay、凭据来源、OAuth 与 request auth 的组合规则会回到 SDK、session、CLI 和 extension;它隐藏的实现很多,但兼容 facade 也使 interface 成本上升。 AgentSession给 interactive、print、RPC 和 SDK 很高 leverage,却同时公开较多产品状态;ExtensionAPI更是宽 interface。它们是有效 module,不应仅因代码多就称为 deep module。ByteTransportFactory有 Unix adapter 和测试 adapter,client lifecycle tests 通过同一 seam;这比“存在一个 transport interface”更能证明替换性。
因此,Pi 的 package 拆分只是线索。一个 module 是否真正变好,要看调用者需要知道的知识是否减少、修复是否集中,以及测试能否只穿过同一 interface 固定行为。
对准备自研 Agent 的工程师,真正值得带走什么
Section titled “对准备自研 Agent 的工程师,真正值得带走什么”1. 按变化轴抽象,不按名词抽象
Section titled “1. 按变化轴抽象,不按名词抽象”可见历史中,provider 协议相关提交先于 Agent core/coding product 的稳定分层;remote/durable seam 又更晚出现。这个顺序支持“按已经暴露的变化轴抽象”,但不能单凭提交顺序还原全部产品动机。Provider、Agent、Session 这些名词本身不会告诉你边界应放在哪里。
2. 用失败路径衡量架构,而不只看 happy path
Section titled “2. 用失败路径衡量架构,而不只看 happy path”最有价值的后续修复都在失败边界:tool batch 中途 steering、length-truncated tool call、truncated compaction、stale snapshot、atomic fork、extension 与 persistence 顺序。一个 interface 是否成熟,要看它能否表达取消、顺序、幂等和部分失败。
3. 测试是 interface 的一部分
Section titled “3. 测试是 interface 的一部分”Pi 中真正可信的 seam 都有真实 adapter 或 conformance test:多个 provider 共享 stream tests,memory/SQLite/JSONL 共享 session 语义,client/server 测 lifecycle 与 protocol failure。仅有 TypeScript interface 或 Go interface,只证明“能编译”,不证明调用者可以安全替换实现。
4. 允许过渡设计,但给它退出路径
Section titled “4. 允许过渡设计,但给它退出路径”agent-old、transport abstraction、compat entrypoint、ModelRegistry facade 都是过渡手段;有的被删除,有的仍保留。过渡层不是耻辱,但要能从历史看出推荐路径、兼容范围和删除条件,否则临时 seam 会永久变成第二套架构。
5. 不要把 Pi 逐行翻译成 Go
Section titled “5. 不要把 Pi 逐行翻译成 Go”可以迁移的是 owner、safe point、narrow seam、conformance test 与 composition root;不能直接照搬的是 TypeScript 结构类型、TypeBox 静态推导、ESM lazy import、动态 extension loader 与 Promise/AsyncIterable 时序。
在 Go 中还必须额外明确:
- channel 的关闭者、buffer/backpressure 与慢消费者策略;
context.Context取消后 goroutine 和外部副作用是否真正结束;- JSON Schema、struct validation 与模型可见 schema 谁是事实源;
- storage 的事务、单写者、跨进程锁和 migration;
- plugin 是编译期 interface、RPC/进程边界,还是动态加载。
结论:Pi 的“现在”来自反复校正,而不是一次正确设计
Section titled “结论:Pi 的“现在”来自反复校正,而不是一次正确设计”Pi 的历史显示了一条比“先拆好三个包”更真实的路线:先闭合产品,再把已出现的差异抽成协议;先尝试较宽的 transport,再收窄为可注入 stream;先把事件都写入 session,再区分暂态与 durable fact;先增加 extension/tool 并发能力,再用 repair commit 固定安全点;最后才把远程协议、存储、遥测和 eval 变成独立 module。
因此,学习 Pi 不应把当前目录当作标准答案。更值得学习的是:每当一个新抽象出现,都去问它隐藏了哪种已发生的复杂度;每当一个修复或 revert 出现,都重新定义这个 interface 真正承诺的行为。Pi 的架构质量,主要体现在这些边界被后续测试不断收紧,而不是 package 数量不断增加。