
Pi 源码解析(五):AgentHarness 如何管理状态和会话
Pi 源码解析(五):AgentHarness 如何管理状态和会话
Pi Agent Core 源码系列(5/8)|上一篇:流式响应与工具执行|下一篇:Session 会话树与持久化
AgentHarness 把 Session、模型配置、工具、队列、事件、Hook 和上下文压缩接到同一套运行流程中。它直接调用 Agent Loop,不重复实现模型和工具循环。
它主要处理三件事:每个 Turn 读到哪些状态,消息和配置变化按什么顺序写入 Session,以及错误或取消后何时才算真正结束。
AgentHarness 的职责
Pi 的 Agent 体系可以分成三层:
三层的分工如下:
| 层次 | 负责的事 |
|---|---|
| Agent Loop | 模型何时调用、工具如何执行、什么时候进入下一 Turn |
| Agent | 如何在内存中维护消息、流式状态、取消和基础队列 |
| AgentHarness | 如何让多次运行共享 Session、配置、资源、扩展和持久化语义 |
Agent 和 AgentHarness 都直接使用 Agent Loop,它们不是上下级嵌套关系。Agent 更适合轻量运行,AgentHarness 则面向完整应用。
Run、Turn 和 Session
Run、Turn 和 Session 对应不同的时间范围。
- Session 可以跨越很多次用户请求,并且能够恢复和分支。
- Agent Run 从一次
prompt()开始,到agent_end结束。 - Turn 是一次 assistant 响应,以及这次响应触发的全部工具执行和结果。
一个 Harness 通常贯穿整个 Session:
1 | 打开 Session |
因此,Harness 通常按会话隔离,而不是作为所有用户共享的全局单例。模型注册表、无状态工具定义和执行环境可以共享,但 Harness 自身包含会话级可变状态。
三类状态
Harness 同时维护实时配置、Turn 快照和 Session 持久化状态。三者的生效时间不同。
Harness 实时配置
实时配置代表“下一轮应该使用什么”,包括:
- 当前模型与 thinking level;
- 已注册工具和当前 active tools;
- system prompt 与资源;
- Provider 请求选项;
- steering、follow-up 和 next-turn 队列。
UI 或扩展可以在运行期间修改它。getter 会立即看到新值,但当前已经发出的请求不会被追溯修改。
Turn 快照
Turn 快照代表当前这一轮已经冻结的运行条件,包括:
- 本轮模型;
- 本轮可见工具;
- 本轮 system prompt;
- 本轮消息上下文;
- 本轮资源与请求选项。
它是模型请求、工具校验和工具执行共同遵守的契约。
Session 持久化状态
Session 记录已经提交的事实:
- user、assistant 和 toolResult 消息;
- 模型、thinking level 和 active tools 的变化;
- compaction、branch summary、label 和当前分支位置。
下一份 Turn 快照从 Session 和 Harness 最新配置重新构建,而不是继续依赖一份不断漂移的临时消息数组。
为什么当前 Turn 必须使用快照
假设 Turn 1 开始时的配置是:
1 | 模型:Claude |
请求发出后,用户切换到 GPT 并禁用 bash:
1 | Harness 最新配置:GPT + read |
Claude 已经看到 bash 的工具定义,因此它返回 bash tool call 是合法行为。如果工具执行阶段读取实时配置,就会出现自相矛盾:
模型按照请求时的工具定义做出了正确决定,执行阶段却用另一套规则否定了它。更危险的情况是同名工具在运行中更换了参数 schema 或实现:调用可能被新版 schema 拒绝,也可能以错误语义执行。
快照把一致性边界固定在 Turn:
规则如下:
1 | 一个 Turn 内配置固定 |
如果用户同时发送 steering 消息,它也会在当前 Turn 完整结束后,由下一份快照处理。当前 Provider 流和已经开始的工具不会被中途换模型或换规则。
快照通常是顶层结构复制,而不是任意对象的深拷贝。工具定义应当视为不可变值,通过 Harness setter 替换,不应在运行中原地修改现有工具对象。
createTurnState() 把 Session、Resources 和 Harness 实时配置汇合成一份 Turn 快照。下面的节选展示了快照从哪里取值:
1 | private async createTurnState(): Promise<AgentHarnessTurnState<TSkill, TPromptTemplate, TTool>> { |
完整实现见 agent-harness.ts。实际返回值还包含请求选项的浅复制;这里保留了决定上下文、模型、工具和资源一致性的字段。
一次 prompt 的处理过程
一次请求经历准备、运行、持久化、save point 和结算五个阶段。
请求中有三次明确的切换:
- 启动边界:读取 Session 和实时配置,生成本轮快照。
- Turn 边界:当前模型和工具完成后,提交运行期间的变化。
- 结算边界:底层 Loop 已结束,待写数据和外部监听器也完成。
Harness 从 Session 读取当前分支
Harness 不直接维护会话树。它在创建 Turn 快照时调用 Session.buildContext(),得到当前活动分支的消息、模型、thinking level 和 active tools;消息结束后再把新记录追加回 Session。会话树、活动 Leaf 和存储实现都由 Session 处理,下一篇 Session:会话树与持久化 会单独展开。
save point 保证写入顺序
模型消息必须按照因果顺序持久化:
1 | user |
但用户可能在 assistant 流式生成期间切换模型。如果立即把 model_change 写入 Session,历史可能变成:
1 | user |
这错误地暗示 Claude 的消息是在切换 GPT 后生成的。
Harness 分两步处理运行中的配置变化:
save point 表示:
当前 Turn 的正式消息,以及本轮期间已经接受的待写修改,都已经按照确定顺序提交。
这样写出的历史不会把配置变化插到错误位置。恢复时也只有两种状态:变化仍在 pending 队列,或者已经写入 Session。
handleAgentEvent() 把这个顺序写进了事件处理逻辑:
1 | private async handleAgentEvent(event: AgentEvent, signal?: AbortSignal): Promise<void> { |
这里省略了 agent_end 的结算分支。消息提交发生在广播 message_end 之前,pending writes 则等到 turn_end 订阅者执行后再统一落盘,两种写入因此不会交换顺序。
事件和 Hook:观察事实与参与决策
Harness 对外开放两类扩展能力,它们不应混为一谈。
事件:观察已经发生的事实
事件适合 UI、日志、持久化观察和指标,例如:
- Agent、Turn 和消息开始或结束;
- 工具执行进度;
- 队列、模型、工具和资源变化;
- save point、abort 和 settled。
订阅者可以异步处理,Harness 会等待它们,以维持事件顺序。
Hook:参与即将发生的决策
Hook 适合策略和扩展,例如:
- 在 Agent Run 前补充 system prompt;
- 在请求模型前调整上下文;
- 修改 Provider 请求选项或 payload;
- 阻止工具调用;
- 修改工具结果;
- 取消压缩或分支切换。
可以将二者简单区分为:
1 | subscribe:告诉我发生了什么 |
三种消息队列对应三种用户意图
Harness 没有把运行期间的新消息都塞进一个队列,而是区分处理时机。
| 队列 | 用户意图 | 消费时机 |
|---|---|---|
| steering | 调整当前任务的方向 | 当前 Turn 完整结束后,优先继续当前 Run |
| follow-up | 当前任务结束后再补充一项 | Agent 原本准备停止时 |
| next turn | 留给下一次主动请求 | 下一次 prompt/skill/template 开始时 |
它们共同遵守一个原则:不会打断已经开始的 Provider 流或工具执行,而是在有明确定义的边界注入。
工具注册表与 active tools 为什么分开
工具存在两个不同概念:
1 | 工具注册表 |
这种分离允许应用预先注册完整能力,再根据模式、权限或环境控制模型当前能看到什么。Session 只需要记录 active tool names;真正的执行函数仍由应用在恢复时提供。
运行期间修改工具集合时,规则和切换模型相同:
1 | 当前 Turn 继续使用旧工具快照 |
phase 控制哪些操作可以同时进行
Harness 将操作分成两类。
结构性操作
以下操作会读取或改变 Session 分支、上下文边界或运行快照,因此必须互斥:
- 发起新的 prompt、skill 或 template;
- 压缩上下文;
- 切换会话树分支。
它们只能从 idle 开始,避免两个操作同时改变 Session 的因果顺序。
运行中可以接受的操作
以下操作在运行期间有明确语义,因此可以被接受:
- steering、follow-up 和 next-turn;
- 修改未来 Turn 的模型、思考级别、工具和资源;
- abort。
Harness 没有追求“所有操作任意并发”,而是为每种变化规定一个可预测的生效边界。
压缩和分支切换只在 idle 时进行
Harness 负责保证压缩和分支切换只能从 idle 开始,并负责在摘要成功后提交 Session Entry;它不在普通 Turn 中自行改写上下文。Token 边界、摘要内容和活动分支的投影规则分别属于 Compaction 与 Session,详见本系列第 6、7 篇。
错误、取消和 settled
Harness 尽量让失败也形成正常可观察的生命周期。异常会被归一化为 assistant 失败消息,使 UI、Session 和恢复逻辑仍能看到闭合的消息与 Turn。
取消和优雅结束也需要区分:
1 | abort |
应用如果要释放会话资源或开始结构性操作,应等待 settled 语义,而不是只看到 agent_end 就立即行动。
完整处理流程
前面这些功能都接在这条流程上:
- Session 提供可恢复的历史;
- Turn 快照保证单轮一致性;
- save point 保证消息与配置变化的提交顺序;
- 事件提供观察能力;
- Hook 提供受控修改能力;
- 队列把运行中的新意图放到安全边界;
- phase 阻止破坏因果顺序的并发操作。
几条运行规则
当前请求使用快照,未来请求读取实时配置
UI 和扩展可以在运行中修改设置,但当前模型请求和工具批次不会被中途换规则。
Session 是已经发生事实的真相源
消息、配置变化、压缩和分支位置都以追加式条目表达,从而支持恢复和审计。
消息先提交,外部修改在 save point 提交
这保证 assistant/toolResult 链不会被运行期间的配置变化插入错误位置。
观察和决策分开
事件负责报告事实,Hook 负责在受控位置参与决策。
结构性操作必须互斥
结构性操作互斥,运行中变化则延迟到明确的 Turn 边界生效。
源码阅读顺序
理解设计后,再回到源码会更容易定位细节:
agent-harness.ts:关注 Turn 快照、执行入口、事件处理和 save point;types.ts:理解事件、Hook 结果和 Session 条目;session.ts:理解状态树如何投影成模型上下文;memory-storage.ts:理解最小 Session 存储模型;jsonl-storage.ts:理解追加式持久化与恢复;compaction.ts:理解长会话的上下文边界。
总结
AgentHarness 把一次运行中的状态变化和 Session 写入排成固定顺序:
1 | Session 提供历史 |
事件、Hook、队列和工具都遵守这个顺序。当前 Turn 使用固定快照,运行中的修改在 save point 提交,下一个 Turn 再读取新配置。
- 感谢你的欣赏!



