青雲的博客

第五部:Coding Agent 的控制面

从 AgentSession 的所有权出发,沿 prompt 管线、JSONL 树、runtime replacement、模型设置和 post-run 策略,读清 Coding Agent 怎样把底层 Agent 变成可交互、可恢复配置、可观察的产品运行时。

AgentSession 连接输入管线、会话树、模型设置和运行后策略的 Coding Agent 控制面。
展开阅读路线与实验入口

第四部停在一个很容易产生错觉的位置:Pi 有 session tree,有 compaction entry,也有 AgentHarness 的运行抽象,于是 Coding Agent 好像只需在上面接一个 TUI。固定版本的产品路径没有这么薄。interactive、print 和 RPC 共享的核心对象是 AgentSession,它同时摸得到底层 Agent、JSONL、settings、资源、扩展、工具和模型运行时。

这部分把视角从“Agent 如何运行”换成“产品如何控制一次运行”。我们关心的不是界面提供了多少命令,而是同一条输入在哪些位置会被改写或短路,一条完成事件先通知谁、后写到哪里,以及切换 session 时旧对象什么时候失效。

flowchart LR
  accTitle: 第五部的控制面阅读图
  accDescr: AgentSession 接受多种入口的输入,协调 Agent 与持久化;SessionRuntime 管整套会话替换,Settings 与 ModelRuntime 提供跨会话默认和请求能力,post-run 再决定继续方式
  E["interactive / print / RPC"] --> S["AgentSession.prompt"]
  S --> A["Agent run"]
  A --> EV["Agent events"]
  EV --> S
  S --> J["SessionManager JSONL tree"]
  S --> UI["扩展与宿主事件"]
  S --> POST["retry / compaction / queue"]
  H["AgentSessionRuntime"] --> S
  H --> RE["new / resume / fork replacement"]
  G["SettingsManager"] --> S
  M["ModelRuntime"] --> S

AgentSession 的文件级契约已经列出它的范围:所有 run mode 共用,负责自动会话持久化、模型与 thinking level、压缩、bash 和会话操作。第 25 章会用构造函数和事件 handler 检查这份声明是否真的落到所有权上。

建议按事件顺序读,不按文件大小读

agent-session.ts 超过三千行,从头扫到尾很容易把 getter、UI helper 和核心状态机混在一起。本部采用一条更短的路线:

  • 第 25 章先画出 AgentSession 持有哪些对象,再沿 message_end -> persistence -> post-run -> agent_settled 建立结算顺序。
  • 第 26 章回到输入端,按实际分支追踪 extension command、input hook、Skill/模板、streaming queue、认证、预压缩与 before_agent_start
  • 第 27 章打开 JSONL,区分物理行顺序、parent-linked tree、current leaf 和最终给模型的 Context projection。
  • 第 28 章将同文件 branch 与产品级 resume/fork 拆开,重点看旧 runtime 的 abort、shutdown、dispose 和新 runtime 的重绑。
  • 第 29 章对照 Agent 当前模型、session change entry 与 settings 默认值,说明三层状态为何看似重复。
  • 第 30 章用一次失败终态收尾,串起 retry、overflow compaction、队列 continuation、累计用量和当前 Context 占用。

这六章不会把每节套进相同的博客模板。对象之间的关系不同:prompt 是有短路的管线,JSONL 是树与投影,runtime replacement 是生命周期序列,usage 则是两个统计口径。结构应当跟着源码走。

证据 checkout 与运行副本分开

第一,session persistence 不等于 run persistence。v0.83.0 的 SessionManager 保存已追加 entry、配置变化和摘要;当前 AbortController、重试等待、工具进程、stream offset 与未结算 Promise 仍在内存里。第 28 章说“resume”时,指从 JSONL 投影重建新的 AgentSession,不是从进程中断点继续一条正在执行的命令。

第二,静态证据与运行验证使用不同目录。章节中的 SourceEvidencegit show | nl -ba 始终读取干净、只读的 tag v0.83.0,commit 为 845d6ff1f6643aba440341cce877ce1c43ebbc39;依赖、dist 和测试输出不会写进这份证据 checkout。

完整运行验证使用同版本的官方 release source archive pi-0.83.0-source.tar.gz,先核对 SHA-256 f225b87ec3b4825dd5b94e922a8629558addca31a1b4d2c206ae598a8e2692c0,再解压到一次性目录。临时副本完成了 npm ci --ignore-scripts、离线 build,以及本册固定测试集:8 个 Vitest 文件的 155 项测试、4 个 TUI test 文件的 288 项测试全部通过,共 443 项。验证结束后临时目录被删除,脚本再次确认固定证据 checkout 没有变化。

这 443 项为本部涉及的 Agent loop、session tree、文件 mutation queue、extension runner 与终端主链提供了运行证据,覆盖范围仍有限。真实 provider 凭据和模型组合、跨平台 binary 安装、所有终端实现,以及崩溃后恢复正在运行的 turn,都不由这组测试证明。

AgentSessionRuntime 对 replacement 的注释也把边界写得很清楚:旧 runtime 先销毁,再创建和应用新 runtime;创建失败由宿主接管,没有隐含 rollback。

查阅时从哪个问题进入

如果你在嵌入 SDK,先读第 25、26 章:一个 session.prompt() 返回前会等到什么边界,哪些输入可能根本不触发模型。若在做会话浏览、导入或分叉,读第 27、28 章:getEntries()getBranch()buildSessionContext() 不是同一个视图,runtime fork 也不是移动 leaf。若在排查“切模型后为什么恢复成另一个值”,从第 29 章的三层状态开始。上下文百分比、重试倒计时或成本对不上时,第 30 章会解释累计历史与活动 Context 为什么不能共享一个数字。

整个第五部仍围绕 v0.83.0 已交付的 Coding Agent 路径。仓库里的后续设计、moving main 或 Harness v2 可以提供演进背景,不能反向改写这里的行为。第 25 章先从最稳定的事实开始:AgentSession 到底持有什么,又在哪个时刻把一次低层 Agent run 判定为真正 settled。

深入浅出 Pi 第五部:Coding Agent 的控制面 第 25 章

`AgentSession` 如何成为 Coding Agent 的控制面

沿构造、事件投影、持久化与 post-run 收口路径,解释 AgentSession 为什么不是 Agent 的别名,而是 Coding Agent 各种运行模式共用的控制面。

源码版本
v0.83.0
验证日期
Commit
845d6ff1f6643aba440341cce877ce1c43ebbc39

前四部已经拆开低层 Agent、两层循环、会话树和保存点。真正启动 pi 之后,interactive、print、RPC 三种入口并不直接拼这些零件。它们共同依赖 AgentSession

源码开头对这层的描述很直白:它服务所有运行模式,负责 Agent 状态访问、事件订阅与会话持久化、模型和 thinking level、压缩、bash,以及会话切换。这里的“控制面”不是仓库里的类名,而是对所有权的概括:谁能同时改变运行状态、持久化记录和后续策略,谁就在控制这场 Coding Agent 会话。

flowchart LR
  accTitle: AgentSession 控制面的所有权
  accDescr: 各种产品入口调用同一个 AgentSession;它向下协调 Agent、会话与设置,向外投影扩展和 UI 事件,并在运行结束后执行恢复策略
  M["interactive / print / RPC"] --> S["AgentSession"]
  S --> A["Agent: 当前运行状态与循环"]
  S --> J["SessionManager: JSONL 树"]
  S --> G["SettingsManager: 跨会话默认值"]
  S --> R["ResourceLoader / ExtensionRunner"]
  S --> MR["ModelRuntime"]
  A --> E["AgentEvent"]
  E --> S
  S --> X["扩展事件"]
  S --> U["宿主 UI 事件"]
  S --> P["retry / compaction / continue"]

构造函数接过的不只是一个 Agent

AgentSessionConfig 要求调用方提供 AgentSessionManagerSettingsManager、cwd、ResourceLoaderModelRuntime,还可以加入 scoped models、自定义工具、工具 allowlist/denylist 和 extension runner 引用。底层 Agent 负责一场 run 的消息与工具循环;它并不知道 JSONL 放在哪里,也不拥有全局 settings。

构造过程只做三件有长期影响的事。它保存这些依赖,订阅 Agent 事件,安装工具和 next-turn hooks;随后 _buildRuntime() 建立内置工具定义、ExtensionRunner 和实际暴露给模型的工具表。扩展 runner 的可变引用也在这里更新,因此 extension hook 和 Agent 请求看到的是同一轮 runtime。

_buildRuntime() 进一步说明这不是简单包装。它读取 settings 中的图片、shell 前缀和 shell 路径,创建全部内置工具定义;再用当前资源加载结果建立 ExtensionRunner,绑定控制方法,最后按默认四工具或显式策略刷新 registry 与 system prompt。此时 Agent.state.tools 才得到 Coding Agent 认可的一组工具。

一条完成事件会同时走向三个消费者

Agent 的 message_end 到达后,_handleAgentEvent 先把事件发给扩展,再通知 AgentSession 的外部监听者,最后按消息角色追加到 SessionManager。普通 user、assistant、toolResult 进入 message entry;extension custom message 进入 custom message entry;bash、compaction 与 branch summary 由各自的专用路径持久化。

这个顺序很值得记。UI 收到 message_end 时,当前 handler 后面的 appendMessage() 还未执行。事件代表底层消息已经完成,不是磁盘已经写完的 receipt。另一方面,handler 会先持久化 assistant error,再把它记录为 _lastAssistantMessage,之后 post-run 才可能为了 retry 或 overflow recovery 从活动上下文移除它。于是历史里保留失败尝试,下一次模型 Context 可以不带那条失败消息。

因此不能从“屏幕上已经显示回答”反推“session 文件已持久化”,更不能反推“这场运行可恢复”。Pi v0.83.0 保存的是完成后的 session entry;AgentSession 里的 AbortController、重试计数、活动工具进程和等待 Promise 都是内存状态。

一次 agent_end 之后,控制面可能还没结束

_runAgentPrompt() 调用底层 agent.prompt(),然后循环执行 _handlePostAgentRun()。后者先检查普通瞬时错误的自动重试,再检查压缩;两者都可能返回 true,让控制面调用 agent.continue()。即使没有 retry 或 compaction,agent_end 扩展 handler 也可能刚刚加入 steering/follow-up 消息,队列仍需再跑一轮。

只有这些 continuation 都结束,finally 才清掉本轮 system prompt override、冲刷待写 bash 消息,并发出 agent_settled。对宿主而言,agent_end 是一次低层 run 的边界;agent_settled 才是 AgentSession 完成 post-run 工作后的边界。

可以用固定 tag 做一次不触碰配置的调用链复核:

repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
git -C "$repo" show v0.83.0:packages/coding-agent/src/core/agent-session.ts |
  nl -ba | sed -n '303,401p;594,665p;1061,1103p;2548,2600p'

这条实验能证明静态顺序,不能证明一次真实模型请求已经落盘。要观察运行行为,需要安装该 tag 的依赖并使用 faux provider 测试或真实 provider;后者还牵涉凭据和网络,本章没有执行,也不把它写成验证结果。

AgentSession 的轮廓至此成立:它把底层执行、产品配置、持久化和恢复策略接到一个会话句柄上。下一章进入最常用的入口 prompt(),看一行用户输入为什么要经过多次短路和改写,才会成为第一条 user message。