跳转到内容

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 基础设施曾并行推进,因此日期会交叠;箭头表示主叙事顺序,不表示单一因果链。

这条主线可以概括为七步:

  1. 先把 OpenAI、工具、session、CLI/TUI 组在一起,完成一个能用的 coding Agent。
  2. provider 增多后,先抽消息、模型、流和工具 schema,而不是先造一个万能 Agent framework。
  3. 再把通用 loop 与 coding 产品拆开;第一次 transport 方案并非终点,后来收敛为更窄的 StreamFn。
  4. session、compaction、extensions 和工具调度逐渐从“功能”变成有顺序约束的状态机语义。
  5. 删除旧实现和旁支产品,同时用 compat 与 revert 控制公开 API 收窄的代价。
  6. 将模型、鉴权和 provider overlay 收到 ModelRuntime,并新增 eval harness,让质量反馈不只依靠单元测试。
  7. 最后才出现独立的 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 并在 Agent constructor 中创建 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 的前后树同时发生三件事:

  1. 原 packages/agent 的 CLI、renderer、session、tools 与旧 Agent 被保留到 packages/agent-old;
  2. 新 packages/agent 定义 AgentState、Agent、AgentTransport、ProviderTransport、AppTransport 与单元/E2E 测试;
  3. 新 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,而是先前抽象开始被并发、重入和副作用击穿,随后由修复测试收紧。

提交 历史源码与测试固定的行为 对代码质量的具体改善
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 已交付”。

当前根 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,可以得到更谨慎的判断:

  • 删除 StreamFn seam,provider 调用与 mock/faux stream 的知识会扩散回 Agent Loop 和每个宿主;真实 provider path 与测试 adapter 都穿过它,因此它提供了 leverage 和 locality。
  • 删除 SessionStorage / SessionRepo seam,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 是否成熟,要看它能否表达取消、顺序、幂等和部分失败。

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 会永久变成第二套架构。

可以迁移的是 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 数量不断增加。