编者摘要:Durable AgentHarness 是具备崩溃恢复能力的 AI 会话运行框架,仅兼容读取旧版 coding‑agent v3 JSONL 会话,不做数据迁移脚本。会话由对话树、执行通道 Lane、Lane 操作日志、全局事实 4 部分组成。 核心单元 Lane 为对话树命名执行位点,单 Lane 同一时刻仅跑一个操作,多 Lane 可并行;支持 Run 运行、Compaction 上下文压缩、Navigation 会话导航三类持久化操作。遵循意图先写、结果后落盘,崩溃后从安全边界恢复,杜绝中间部分生效状态。
框架区分钩子(修改执行逻辑)与事件(仅观测),支持 manual 手动步进驱动,便于模拟崩溃做确定性测试。存储支持内存、JSONL、SQLite,单会话强制单写入者。提供延迟服务商请求能力,句柄持久化,崩溃后可兑现获取结果。
配套完整 API、快照订阅观测、链路埋点。测试分三层:恢复逻辑、执行轨迹校验、手动驱动竞态 & 崩溃模拟。开发拆分为多任务包,明确依赖,不改动 coding‑agent 源码。
7 个关键问题问与答
Q1:向后兼容策略是什么,会做旧版本数据迁移脚本吗?
A:仅保证能打开并恢复 coding‑agent v3 JSONL 会话至空闲;不编写迁移、版本 Schema、转换脚本,其余 API、存储格式允许破坏性变更。
Q2:什么是 Lane 执行通道,它解决什么业务场景?
A:Lane 是对话树上命名执行位点,单 Lane 同一时刻只执行 1 个操作,同会话多条 Lane 可并行。用来实现会话内多线程,如 Slack 会话下多条聊天线程、子代理在父会话内并行执行。
Q3:系统如何保证崩溃之后不会出现部分生效的中间状态?
A:遵循先写意图记录,后写结果条目。崩溃发生在两者之间,恢复逻辑根据意图判断:重试、补全或者生成合成结果;不存在半完成对外可见状态。
Q4:钩子 Hook 的副作用能做到恰好执行一次吗?
A:不能。钩子结果只有提交持久化记录才落地;提交前崩溃钩子会被重复执行。需要外部副作用的钩子业务必须自己实现幂等,例如绑定操作 ID。
Q5:Deferred 延迟请求如何实现崩溃恢复?
A:服务商返回句柄,句柄持久存入助手消息;进程崩溃恢复 resume 时识别未兑现句柄,重新拉取结果,不会重新发起全新大模型请求。
Q6:manual 手动驱动模式作用是什么?
A:将每一个实际效果执行前暂停。测试可以逐步骤驱动、随时关闭重开模拟进程崩溃,生产和测试复用同一套业务逻辑,保障确定性。
Q7:存储层单写入者约束,具体如何保障?
A:业务层 Harness 层面:一会话同一时间仅分配一个 Harness 实例;SQLite 增加过期写租约;JSONL 依靠上层服务路由;存储检测多写判定故障,拒绝写入。
附录 Durable AgentHarness 设计方案
兼容性策略
旧版 coding‑agent v3 JSONL 会话文件必须能够正常打开,并可恢复至空闲状态,这是唯一向后兼容要求。除此之外,packages/agent/src/harness、packages/session‑backends/sqlite‑node下的全部格式、API 及其配套测试均允许发生不兼容变更。本设计不提供任何迁移脚本、模式版本管理以及数据转换逻辑。
Harness 针对单个会话执行任务;会话维护四类状态(详见第 2 章节)。同一个 Harness 内部可并行运行多条执行通道(Lane,详见第 3 章节);存储后端负责会话数据的编码持久化(第三部分)。
第一部分 — 核心概念
1. 设计目标
持久化运行实例:已接收的提示词属于持久化操作。进程崩溃后,新进程可恢复会话状态,从上一个安全边界继续执行;崩溃所能产生的全部状态均支持恢复。
执行通道(Lane):一个会话可包含一条或多条 Lane。Lane 代表对话树中一个命名执行位点,单条 Lane 同一时刻最多执行一项操作,多条 Lane 之间支持并行运行。一次运行及其排队消息隶属于接收该任务的 Lane。
示例:一个 Slack 频道对应一个会话,频道内每条线程即为一条 Lane。交互式 Pi 仅使用单条 Lane,UI 层不会暴露该概念;扩展组件可调用完整 Harness API,直接使用 Lane 能力。例如子工具代理,会在父会话的第二条 Lane 上运行。
杜绝部分生效状态:运行、压缩、导航任意操作发生崩溃,系统只会呈现两种结果:操作完全未执行,或恢复机制可完整补全该操作;不会对外暴露中间状态。
Harness API:事件机制用于观测执行流程,无法修改执行逻辑;钩子(Hook)机制可以拦截执行过程,支持修改上下文、请求、工具调用、运行边界。扩展组件基于事件与钩子进行开发。
确定性步进执行:所有会产生实际效果的行为 —— 持久化写入、服务商请求、工具执行、钩子回调、定时器,都会经过显式注入的执行边界。当驱动模式设置为 drive: "manual",Harness 会在每一次实际效果执行前暂停,测试代码可以逐次驱动执行:在任意边界暂停、注入输入、关闭再重新打开进程,以此模拟崩溃场景。生产环境与测试环境复用同一套执行逻辑,驱动模式仅控制执行边界行为(详见第 15 章节)。
可观测性:全部执行链路均可接入日志与链路追踪,粒度可下探至服务商请求与响应内部实现。该观测通道与钩子系统相互独立。
UI 交互模型:客户端先获取一份原子快照,之后消费实时事件流;事件不会重放。重连客户端需要重新获取全新快照。
单写入者约束:同一时刻,仅有一个 Harness 实例可对一个会话执行写操作,由服务层强制执行。会话全部 Lane 归属该 Harness 实例。恢复逻辑将单写入者不可能产生的状态判定为数据损坏。
兼容 v3 会话:旧版 coding‑agent v3 JSONL 文件可直接加载,恢复至空闲状态。
非设计目标
- 钩子侧效果至少执行一次
钩子执行结果,只有在消费该结果的记录 / 条目完成持久化提交后,才会真正落地。提交前发生崩溃,钩子会被重复执行(详见第 11 章节重放对照表)。钩子自身发起的外部调用(HTTP、文件写入)对 Harness 无感知。若钩子需要崩溃安全的外部副作用,必须设计为幂等,例如绑定操作唯一 ID。 - 服务商流式请求断点续传
流式请求的中间分片不会持久化;中断的流式请求要么重试,要么直接放弃。延迟请求不在此列:服务商立刻返回句柄,后续再返回结果(例如 Responses API 的 background: true、批量接口)。Pi‑AI 会返回一条助手消息,停止原因为deferred,消息携带句柄并正常持久化。兑现句柄后追加一条标准助手消息。恢复逻辑识别未兑现句柄,直接拉取结果,不会重新发起全新请求。 - 多写入者
不支持两个进程同时读写同一个会话。服务层会将会话的全部流量路由至持有该会话 Harness 的进程。Lane 机制用于模拟看似多写的业务场景:基于共享历史执行多条并行线程。 - 数据副本同步复制
会话数据只存储在一处;无协调的多副本双向同步属于另一套独立设计,本方案不排斥后续叠加该能力。 - 迁移 coding‑agent
将 coding‑agent 改造适配 AgentHarness 不在本方案范围;兼容性仅指新 JSONL 仓库能够读取受支持的 v3 文件。
2. 会话定义
会话是具备持久化能力的状态集合,由四部分构成:
- 对话树(Tree)
完整对话内容。由携带 parentId关联关系的条目构成,包含消息、模型 / 思考 / 工具调用变更、压缩摘要、分支摘要、自定义条目。对话树为共享被动数据,不归属于任意 Lane,只做追加写入,条目永远不会被修改或删除。 - 执行通道(Lanes)
实际业务执行载体。每条 Lane 包含名称与叶子位点:即后续工作将要接续扩展的条目。每个会话默认内置 main通道;应用可基于外部业务标识(Slack 线程 ID、邮件线程 ID)创建更多 Lane。 - Lane 操作日志
记录发生的事件与待执行任务。每条 Lane 维护一份时序扁平记录序列:操作启动、步骤尝试、工具启动、消息入队、操作完成。持久化能力依托该日志实现:进程崩溃后,新进程读取日志,继续完成 Lane 的任务。正常执行流程不会读取该日志。 - 全局事实(Global facts)
会话级别的键值数据,遵循 “后写覆盖” 规则,例如会话名称、条目标签。不属于对话树;采用追加历史存储,读取时取最新版本。
四部分所有写入操作共享一套单调递增序列号;该序列号为全局事实提供时序排序,同时让 Lane 操作日志可以引用对话树的位点。
示例结构
对话树(共享,仅追加) Lanes
a ── b ── c ── d main → d (操作日志:……)
└── e ── f slack:171943… → f (操作日志:……)
全局事实:name = "Refactor auth", label(b) = "checkpoint‑1"
主动状态与被动状态
对话树、全局事实属于被动状态,属于共享可读数据。 Lane 属于主动状态,持有自身叶子位点、操作日志(最多一条运行中操作)、消息队列、待落地写入。多条 Lane 之间不会共享以上资源。Lane 的每一次动作,要么生成挂载在自身叶子后的对话条目,要么写入自身专属操作日志。
不变性约束
-
对话树仅存储对话内容,不存放 Lane 状态、编排状态、各类指针。 -
条目的父链永远不可变更;分支共享历史前缀,不会复制拷贝条目。 -
Lane 的叶子位点只有两种变更途径:Lane 追加新条目,叶子指向该条目;或者执行导航操作,直接跳转至某个已有条目。 -
操作日志记录不会修改对话树。即便删除全部操作日志,对话依然完整有效。 -
单条 Lane 最多只能存在一条未完成操作;一条 Lane 出现两条未完成操作判定为数据损坏。 -
条目是会话共享资源;日志记录归属于唯一 Lane。多条 Lane 的执行路径可以复用同一条条目;一条记录只能属于某一条 Lane。 -
日志记录不等同于对话条目:记录描述执行过程,不属于对话内容,不会进入模型上下文、转录文本、分支查询、分支拷贝。单条 Lane 内部,记录时序本身就表达顺序,无需 parent 关联。
3. 执行通道(Lane)
Lane 是对话树上的命名位点,绑定序列化排队的业务任务。类比 Git 工作树检出分支:命名绑定一个提交点;新工作推进该位点;可以跳转至任意已有条目,不修改历史记录;同一分支不会被重复检出。与 Git 的区别:导航可以跳转到历史任意条目,不限于向前移动。
每个会话自带 main通道。应用指定锚点条目与名称即可创建新 Lane;Lane 名称为业务永久键值,如 Slack 线程 ID。UI 不会直接罗列抽象 Lane;由平台线程列表承担该展示能力。
Lane 持有的资源
- 叶子位点(leaf)
新条目会挂载到此位点,更新叶子;导航操作直接跳转叶子。 - 操作日志
最多一条处于打开状态的操作。Lane 繁忙时,收到第二条操作请求直接拒绝,不影响其他 Lane。 - 各类队列
重定向指令、后续跟进消息、下一轮运行消息,全部归属对应 Lane。 - 配置视图
模型、思考等级、激活工具配置,读取 Lane 叶子路径上的历史配置条目。多条 Lane 可以使用不同模型,互不感知。工具实现、资源、流选项属于 Harness 全局配置;仅工具激活状态按 Lane 隔离。
运行规则
-
多条 Lane 可并行执行操作;Harness 依然维持单写入者,不同 Lane 的记录、条目会交错写入共享序列号序列。 -
创建 Lane 不会复制任何数据;Lane 不支持删除、重命名。 -
Lane 内部状态变更操作,会在该 Lane 的变更队列上线性执行:校验、最多一次持久化写入、内存状态更新全部完成,才会处理下一次变更。服务商请求、工具调用、钩子、重试逻辑不在变更队列内执行。 -
两条 Lane 当前叶子位点相同时,下一次追加就会自然分叉,由对话树处理分支逻辑,Lane 之间无需协同。 -
存在未完成操作的 Lane,恢复后处于挂起状态,和其他 Lane 相互独立。挂起原因分为进程崩溃、服务商延迟请求(详见第 1 章节)。
4. 任务如何执行
操作(Operation):Lane 上持久化工作单元
分为三类:
- 运行(Run)
已接收的提示词,包含全部自动接续逻辑:工具调用、指令重定向、后续消息、自动上下文压缩;无待处理任务代表运行结束。 - 压缩(Compaction)
用一条摘要条目替换旧上下文。 - 导航(Navigation)
将 Lane 叶子跳转至已有条目,可选择性生成分支摘要。
操作需要先被 “接收” 才开始执行;接收动作本身会持久化。崩溃恢复后,已接收操作要么被恢复完成,要么被显式关闭。每条 Run 最终状态只能是 completed、failed、aborted;压缩、导航额外存在 declined,代表钩子在实际生效前否决该结构化操作。
运行、轮次、步骤
- Run(运行)
由若干轮次(turn)组成。 - Turn(轮次)
一次助手回复 + 该回复触发的完整批量工具调用。 - Step(步骤)
操作内部可重试的最小单元:生成助手消息、生成压缩摘要、生成分支摘要。一个步骤可以发起 0 次、1 次或多次服务商请求。失败的尝试会重试同一个步骤;尝试次数会持久化,重启进程不会重置计数。
延迟服务商请求会终止助手步骤:持久化携带句柄的助手消息,步骤关闭,操作整体挂起;后续兑现句柄再追加真实返回结果(详见第 1 章节)。
每一条发起实际效果的工具调用同样是一个步骤。tool_started标记启动;工具结果条目完成关闭。批量工具可以同时开启多个工具步骤;并发执行,最终结果按照原始输入顺序落盘(详见第 14 章节)。
队列与延迟写入
两种向运行中 Lane 投递输入的机制,中止行为存在差异:
- 队列
承载对话意图: steer修改当前任务、followUp在模型停止后追加任务、nextRun为下一次运行预置消息。steer、followUp在中止操作时会丢弃,载荷返还调用方;nextRun消息可以留存。 - 延迟写入
承载事实数据:步骤执行过程中收到的条目、配置变更。中止操作不会丢失,即使取消任务依然会落地执行。
两者在接收阶段就完成持久化:接收调用向 Lane 操作日志写入完整载荷记录,调用才返回。对话条目不会立刻写入;等到消费 / 应用该数据时才追加。进程崩溃发生在接收和条目写入中间,恢复逻辑读取记录完成追加。已接收输入不会丢失。
检查点(Checkpoint)
每一轮次结束,Lane 抵达一个检查点,执行逻辑:
-
落地全部待处理延迟写入; -
消费队列内的重定向消息; -
如果下一次请求会超出上下文窗口,则执行压缩。
压缩也存在被动触发场景:服务商返回结果提示上下文超限(溢出报错、length停止,且实际输出小于目标上限)。此时丢弃该响应,执行压缩,重试一次(详见第 6 章节「助手步骤上下文溢出」)。
当助手消息携带工具调用,会强制开启新一轮,让模型读取工具返回结果;唯一例外:批量工具所有返回结果都标记 terminate: true,就不会自动继续工具轮次。重定向、后续消息仍然可以手动开启新一轮。检查点没有待处理任务,Run 运行结束。
仅尾部追加上下文
Lane 多次服务商请求,上下文只能在尾部增长。如果在上一次请求上下文中间插入新消息,会直接使服务商 KV 缓存失效,大幅增加 Token 开销。
该约束解释为什么轮次中间的写入会延迟到检查点统一执行:检查点统一在尾部追加。压缩是唯一刻意打破该约束的操作,以一次缓存失效换取更小的上下文占用。
Lane 生命周期
状态归属于单条 Lane。例外:存储写入失败会让整个 Harness 故障。故障的 Harness 停止所有效果,拒绝全部调用;问题修复后重新打开会话,各条 Lane 从日志记录恢复状态。
- Suspended(挂起)
存在一条未关闭操作,暂停执行。产生场景:崩溃恢复、持久化延迟句柄。调用 resume()继续执行;调用abort()关闭操作,不再继续运行。
abort()将取消动作持久化,向正在运行的效果发送中止信号,然后直接返回;随后执行状态调和逻辑。自动驱动模式后台完成调和;手动驱动模式会停在下一个动作位点。
Resume(恢复):继续执行未完成操作,不会启动新操作。恢复入口由日志记录终点决定:重试未完成步骤、兑现延迟句柄、调和半完成工具批量、或者直接进入下一个检查点。崩溃前入队的消息、延迟写入会保留,正常应用。
5. 记录(Records)
持久化准则
执行效果之前:写入意图记录,写明即将执行的动作以及将要生成的 ID;执行效果完成后,追加携带完全相同 ID 的结果条目。
多条记录之间不提供原子性,设计上也不需要。单条记录、单条条目独立持久化。崩溃发生在意图记录和结果条目之间,会留下未完成意图;恢复逻辑根据意图类型决定:补全执行、重试、或者生成合成结果。意图完成的判定条件:存在携带预分配 ID 的条目。
条目内容与意图记录预分配 ID 内容不一致,判定为数据损坏。
ProvisionedEntry:预分配 ID 的条目载荷;parentId、seq、timestamp由存储层追加条目时分配,挂载到 Lane 当前叶子位点。
完整记录类型、TS 接口定义参考原文文档。
关键说明:
-
被拦截、校验失败的工具调用不会生成 tool_started记录;不会产生实际效果,因此无需意图记录;会直接生成带错误标记的工具结果条目。崩溃发生在写入该条目之前,该决策会丢失;恢复阶段会再次运行before_tool。 -
工具步骤不需要单独的结果记录;工具结果条目就是完整持久化输出,包含批量控制标记 terminate。执行完毕但还没写入结果条目就崩溃,会按照重放策略处理(第 6 章节);恢复会重新运行after_tool,该行为在第 1 章节非目标中已明确允许。 -
Token 计费是特例:计费完整性不能依赖结果条目。重试步骤会产生失败尝试,这些尝试不会生成对话条目,但消耗的 Token 不能丢失。因此每一次服务商请求完成,优先写入 usage 计费记录,之后再做分类、重试判断、丢弃响应。工具、钩子上报消耗也会生成对应记录。应用可以写入 adjustment类型记录用于无法自动统计的场景。 -
条目内部 usage字段是响应的不可变快照,写入后不再修改;读取时的真实消耗 = 查询所有绑定该 entryId 的 usage 记录(原始记录 + 调整记录总和);会话总消耗为全部 usage 记录求和。恢复阶段允许出现重复计费:重试步骤、重放工具都会各生成一条计费记录。
恢复判定为数据损坏的场景
-
Lane 存在多条打开操作; -
记录引用不存在或者已经结束的操作; -
同一个步骤内部尝试编号不连续; -
压缩尝试缺少 compactionReason,其他步骤携带该字段; -
Run 已经收到 abort_requested,后续还出现属于该 Run 的 steer/followUp 入队记录; queue_cancelled指向不存在入队记录,或者对应条目已经生成; -
同一个结构化步骤多次尝试, resultEntryId、compactionReason前后不一致; tool_started内索引无法匹配助手条目原始工具调用信息; -
同一个调用身份生成多条 tool_started; -
预分配 ID 的条目实际落地,但是内容与意图记录不一致。
6. 各类动作写入逻辑
文档使用标记说明: E:向对话树追加条目,挂载 Lane 叶子; R:向 Lane 操作日志追加记录; L:移动 Lane 叶子指针; G:写入全局事实; H:执行钩子(异步等待;钩子属于第一部分概念,API 在第三部分); X:崩溃发生位点。
示例:单次工具调用完整运行链路、重试逻辑、崩溃各个位点恢复规则、上下文溢出处理、工具运行时接收重定向消息、队列取消、延迟写入、中止操作、导航、延迟服务商请求等完整流程与崩溃恢复规则,保留原有逻辑。 核心要点:可恢复的
length截断响应直接丢弃,不会写入对话树;每一次服务商请求无论成败,优先写 usage;一次用户输入最多执行一次压缩‑重试循环,避免死循环。
7. 恢复机制
Restore(会话打开恢复)
打开会话时,每条 Lane 独立恢复。Restore 只做读取,不会追加数据,也不会启动任何执行逻辑。恢复采用索引定位,避免全量扫描日志: 调用 findOpenOperations(lane, { limit:2 })倒序查找未完成的 operation_started。返回 0 条代表空闲,1 条代表挂起,返回 2 条直接判定损坏。
挂起 Lane 的恢复仅读取两份有界数据:
-
自该操作启动之后,本 Lane 的全部记录;之前旧记录无需读取。 -
该操作新增生成的全部条目(从叶子回溯到操作锚点 sourceLeafId)。
额外做少量点查询:预分配 ID 条目、锚点位置的配置信息(模型、思考等级、激活工具)。全部为索引查询,不扫描完整会话历史、不扫描其他 Lane。
空闲 Lane 的剩余状态为未消费 nextRun队列。
规约化简(Reduction)
基于上面两份读取结果,推导 Lane 当前全部内存状态:是否正在中止、已使用的尝试次数、未完成步骤、工具批量状态、延迟句柄、待消费队列、待落地延迟写入、缺失初始消息、结构化操作目标等。
usage计费记录不参与编排逻辑化简,只做统计。
正常运行过程,Harness 每写一条数据同步更新内存状态;恢复阶段完全从存储重新计算得到状态。内存状态是日志与条目的化简产物,二者不能存在矛盾。
Resume(恢复继续执行)
resume()根据化简出来的状态继续运行未完成操作,执行顺序:
-
补全缺失初始消息; -
如果标记中止,执行调和逻辑,生成合成工具结果、关闭助手消息,标记操作中止结束; -
处理未完成工具批量; -
如果存在未兑现延迟句柄,执行兑现逻辑; -
如果检测到终端失败(助手错误消息):落地全部已接收写入、消费队列消息;如果消费队列没有产生新任务,直接标记 Run 失败结束;恢复逻辑不会自动修复这类运行; -
存在未完成步骤:继续该步骤;达到重试上限则标记操作失败;压缩步骤会沿用记录里的 compactionReason; -
其他情况:跳转至下一个检查点,正常应用待处理写入与队列。
恢复阶段追加条目有一条特殊规则:如果预分配 ID 条目已经存在,跳过写入。因此恢复中途崩溃,再次运行恢复依然安全。恢复逻辑只会在策略允许的场景下重复执行动作:可重试步骤开启新的持久化尝试;工具只有记录与当前工具定义同时标记 safe才会重放。中断的钩子处理遵循第 11 章节重放对照表。
v3 旧会话没有操作日志记录;所有 Lane 化简结果为空闲。
main会规范化,叶子定位到经过丢弃条目解析后的最后有效逻辑条目(详见第 12 章节)。
第二部分 — 执行记录规范
本部分后端无关,定义 Lane 写入的全部记录类型、写入时机、恢复读取逻辑;第三部分映射为具体 API 与存储实现。
第三部分 — API 与实现
8. 公开 API
AgentLane代表单条 Lane 的操作接口。AgentHarness本身实现 AgentLane,对应 main通道,例如 harness.prompt()等价于 main 的 prompt。全部方法均为异步,包含 getter 接口;该接口需要支持远程代理封装,因此不暴露只能本地内存同步返回的签名。
同步特例:
name属性、钩子 / 事件监听注册;服务端会做传输层桥接,不会把注册动作暴露到网络。
接口包含:运行操作(prompt /compact/navigateTree /resume/abort);队列接口 steer /followUp/nextRun /cancelQueued;记录消耗 recordUsage;等待空闲;手动驱动控制;持久化配置读写;会话树视图;状态监听 watch。
AgentHarness 类
负责会话打开恢复、Lane 的创建查询、Harness 全局配置、钩子事件、会话级监听、关闭会话。
Lane 名称是业务永久主键,例如
slack:1719432.0021;多个对象句柄可以指向同一个 Lane,对象只是外观代理,身份由 Lane name 唯一确定;Lane 不支持删除重命名。
返回值与带标签错误
对外 API 采用精简版 better‑result v3模式,内部不引入该运行时依赖。区分 Result.ok/ Result.err;自定义带 _tag的 TaggedError 错误类型。
调用返回
Err代表本次调用没有接收 / 创建目标任务。一旦操作被成功接收,无论后续是中止、失败、挂起,接口都返回Ok,通过 outcome 字段区分业务结果。
存储写入失败不会作为业务 Err 返回;会将整个 Harness 置为故障,调用抛出 HarnessFault异常。调用已经 close 的实例,Result 接口返回 Closed 错误;其他接口抛出 HarnessClosed。
SuspendedOperation 挂起操作结构体
AgentHarness.create()返回,描述会话打开后处于挂起的操作,UI 可用来展示恢复、中止按钮。
9. 快照与订阅
UI 需要获取当前完整状态快照,再接收后续增量事件,不能存在数据缺口。考虑网络传输:服务代理实现需要先把快照下发给客户端,之后再推送事件流。
watch():获取快照,同时内部缓冲事件;调用 start(listener)之后,先顺序把缓冲区全部事件推送,再切换为实时事件流。unsubscribe()销毁订阅和缓冲区。未调用 start,事件会无限缓冲。
lane.watch()Lane 维度,包含转录对话、操作状态、队列、待写入数据;事件过滤为当前 Lane。适合单线程渲染(Slack thread)。 watchSession()会话全局,只提供 Lane 清单,不携带转录文本,接收全部原始事件流;适合仪表盘。
LaneSnapshot快照结构体包含:lane 名称、转录文本、叶子 ID、运行操作状态(含流式消息、运行中工具、重试信息)、各队列、待落地写入、全局故障标记。
配置不在快照内,通过 getter 实时读取;
config_update事件通知客户端重新拉取。进程重启恢复后,不会保留内存中的流式分片,快照直接展示 suspended 挂起操作。
10. 事件(Events)
事件流是扁平一维序列。 约束:
-
事件监听回调抛出异常会被捕获,生成 handler_error事件,不会中断主流程; -
事件投递时序与进程执行顺序一致;跨 Lane 不保证严格按照存储 seq 顺序;持久化消费者请直接读取日志接口; - 事件不持久化,不会重放。重连必须重新执行 watch;
-
代表持久化变更的事件,触发时机为数据提交完成之后;事件看到的数据已经可以通过查询接口读到; -
事件返回转换之后的最终值(钩子执行完毕); -
全部载荷 JSON 可序列化,敏感信息已经脱敏;模型、工具只传递名字,不内嵌对象。
事件分类:运行生命周期事件、重试事件、消息事件、工具事件、树 / 队列 / 事实变更事件、配置变更事件、结构化操作事件、Lane 创建事件、计费 usage 事件。附带事件嵌套时序示例。
11. 钩子(Hooks)
钩子是异步拦截回调;注册属于 Harness 全局,每个事件 payload 携带 lane 字段。
before_run/ before_resume注册必须提供稳定唯一 id;同一个扩展组件使用同一个 id;before_run输出的resumeData会以 id 为键持久化;恢复阶段before_resume只读取对应 id 的数据。-
多个同类型钩子按注册顺序串行执行;后一个钩子拿到前一个钩子返回结果。 -
大部分钩子抛出异常仅跳过该钩子,上报 handler_error,继续执行剩余钩子;例外:before_tool抛出异常直接判定拦截工具调用。 - 钩子返回结果本身不具备持久化;只有写入对应的记录 / 条目才算落地;提交前崩溃钩子会重新运行。
-
事件返回的是钩子执行完毕之后的数据,观察者看不到钩子修改前原始内容。
完整钩子清单:运行边界钩子、请求管道钩子、工具钩子、结构化操作钩子。附带重放对照表:Fresh 首次执行、Retry 重试步骤、Resume 崩溃恢复之后是否再次执行钩子。
12. Session 与 SessionTree
条目 Entry
对话树所有条目类型定义:消息条目、模型变更、思考等级变更、激活工具变更、压缩摘要、分支摘要、自定义条目。
关键字段说明:
terminate存放在工具结果条目上;fromHook标记摘要是否来自钩子;retainedTail压缩条目保留的上下文;usage条目为不可变快照。
v3 文件加载规范化逻辑:
custom_message→ 自定义消息条目; label、 session_info转为全局事实,从逻辑对话树移除;-
被丢弃的旧条目,它的子节点重新挂载到最近未丢弃祖先; main的叶子解析到最近有效祖先; -
旧压缩条目 firstKeptEntryId转换生成retainedTail; -
v3 字符串时间戳转换为毫秒 Unix 时间戳; -
只读打开不会改写物理 v3 文件;第一次发生写操作,执行一次性规范化转 v4。
SessionTree 接口
面向对话树读写。每条 Lane 对外暴露一份 lane.session视图。
写入规则:Lane 处于运行中(挂起、中止也算),写操作变为延迟写入;压缩 / 导航结构化操作,等待操作结束再落地;空闲 Lane,直接追加条目。脱离 Harness 的独立 Session,写入立刻生效。 查询语义:分支扫描从 start 向根节点回溯,支持过滤、停止条件、分页游标。上下文构建 = 分支扫描到 compaction 压缩条目为止,经过
entryProjectors、toProviderMessages转换得到发给服务商的消息。
Session 类
继承 SessionTree,增加 Lane、记录日志底层接口。可以脱离 Harness 独立使用,测试、恢复夹具会直接调用底层 API。
重要所有权约定:应用一旦把 Session 传给
AgentHarness.create(),就不能再通过原始 Session 对象并发修改,全部变更必须走 Harness 和 Lane 视图;并发修改属于调用方错误,Harness 不会额外防护。
13. 存储层(Storage)
存储契约
一个存储实例对应一个会话。存储只负责持久化、查询;不执行业务逻辑、队列、恢复逻辑。
-
全局唯一单调递增 seq,跨条目、记录、事实、Lane 移动;存储层在一次原子提交内分配 seq;写 Promise 返回顺序等价提交顺序。Lane 变更队列做业务层串行决策;seq 做存储层串行,二者缺一不可。 findOpenOperations是恢复必须的投影接口,能够区分 0/1/2 条未完成操作。 -
没有通用的 CAS 条件写入;依靠单写入者 + Lane 变更队列避免竞态。 -
写失败存储保证数据是合法的历史前缀,不会出现半写损坏。 -
全局事实、Lane 移动保留全部变更历史;读取取 seq 最大最新记录。
分别介绍内存后端 Memory、JSONL 文件后端、SQLite 后端。SQLite 重点讲 writer_leases 单写锁、branch_entries /branch_tips 分支缓存;appendEntry 的四种分支拷贝 / 扩展场景。
14. Agent‑loop 底层构建块
agent‑loop.ts:无持久化状态的纯执行原语;Harness 在这些原语外层包裹持久化写入边界。
streamAssistant单次助手服务商请求; -
工具调用三阶段:prepareToolCall(校验、before_tool)→ executeToolCall(实际执行)→ finalizeToolCall(after_tool 处理结果); executeToolBatch批量工具驱动器:支持串行 / 并行模式,截断处理、abort 处理、 terminate批量结束标记。
兼容包装:原有
agent‑loop.ts全部对外导出签名不变,测试套件可以直接跑通,内部复用上面新原语,使用空实现 TelemetryContext。
15. Harness 内部实现
所有业务效果全部通过注入的 Effects fx对象调用。
drive: "automatic"fx 直接透传; drive: "manual"每一个 fx 调用会先挂起,返回 ActionInfo;测试代码调用 peekAction()/executeAction()/runToCompletion()一步步驱动。
Effects 接口
分为:持久化写入接口、条件提交接口、外部效果接口、钩子与时间接口。
读取接口不属于 Effects,不会被 manual 模式拦截。manual 模式下暂停的时候,不会发生任何存储写入、服务商、工具调用。
Lane 变更队列(mutation line)
解决 “读取状态‑await‑写入” 产生的竞态条件。每条 Lane 维护一条 FIFO Promise 链。每个 Job 的规则:校验内存 LaneState → 最多一次持久化写入→ 更新内存状态。
服务商请求、工具、钩子、重试逻辑不能放在 Job 内部执行,只能在 Job 间隙运行。保证并发操作只有两种合法时序 [A,B] / [B,A],不会出现交错。
完整竞态目录表格:全部合法时序,以及对应的保障机制。Tier‑C 测试必须覆盖双向时序。
驱动模式实现
GatedEffects封装,挂起动作,缓存待执行动作;提供 peekAction、executeAction、runToCompletion。
重入支持:一个动作执行过程中又调用 fx,新动作继续入队;manual 模式下会逐个释放。Lane 上层接口不受 gated 影响;暂停过程依然可以调用 abort、steer,会立刻进入变更队列执行。
close()挂起状态会拒绝本地 Promise;持久化操作保持原样,重新打开会话 resume 即可继续。
LaneState 内存状态
Lane 运行时内存编排状态,必须等价于化简函数 reduceLaneState的输出。打开会话 restore 通过日志条目调用化简函数得到;正常运行每一次提交同步更新。
过程内部信号
使用内部异常流转控制流:RunFailed、Park、Aborted、Overflow;异常不会向外抛出给调用方;只有存储损坏等严重错误向上抛出,导致 HarnessFault。
核心伪代码逻辑
resume()、runProcedure()、driverLoop()、runTurn()、assistantStep()、recoverOverflow()、工具批处理、工具恢复调和、abortPath、compactionProcedure、navigationProcedure。
关键不变性:恢复逻辑使用
appendIfMissing(target):如果预分配 ID 条目已经存在,跳过写入;内容不一致判定损坏。 每次 resume 完成、挂起、结束,执行化简函数和内存 LaneState 做一致性校验,发现不匹配直接判定损坏,故障整个 Harness。
16. pi‑ai:延迟请求(deferred requests)
服务商接口层支持 deferred请求选项;服务商快速返回句柄 DeferredHandle,而不是完整内容。stopReason: "deferred";消息携带句柄持久化。ProviderStreams 接口增加可选的 fetchDeferred/ cancelDeferred。Models 层封装认证,对外暴露统一接口。
兑现句柄逻辑:一次 resume 最多执行一次 fetchDeferred;返回再次 pending‑deferred 则重新挂起;返回 error 生成错误助手消息,运行标记失败;不会自动发起新一轮模型请求。
17. 会话分支拷贝与子代理
仓库提供 fork()拷贝原语。
scope:"branch"拷贝单条分支历史;fork 后只有 main Lane,叶子在拷贝位点;不会复制操作日志、队列,fork 出来会话全部 Lane 初始化为空闲;计费 usage 记录不会复制;entry 的 usage 快照保留。 scope:"tree"拷贝全部条目、全部 Lane 叶子指针;同样无运行时日志。
父会话 ID parentSessionId记录拷贝溯源关系。子代理工具:子会话 ID 可以由父会话 ID + toolCallId 确定性生成;安全重放可以重新绑定旧子会话,不会重复新建。
区分:Lane = 同一会话内共享历史的并行线程;fork = 完全隔离副本,用于子代理、导出、克隆。子代理既可以 fork 出新会话,也可以直接跑父会话新 Lane,看业务隔离需求。
18. 可观测性(Telemetry)
禁止依赖 AsyncLocalStorage、运行时全局上下文;全部通过显式传入 TelemetryContext参数传递。包划分:
packages/telemetry通用契约、内存参考实现、无空实现、schema 基础工具; packages/agent/src/harness/telemetry.ts定义 Harness 与 AI 请求两套领域 schema; packages/ai仅消费 TelemetryContext 入参,不定义业务 Span schema。
TelemetryContext契约:startSpan()回调模式;回调返回 / 抛出自动结束 span;适配器可以对接 OTel、Sentry;核心不绑定任何第三方埋点后端。
定义 Span 属性 schema;AI‑request 埋点、Harness 全套埋点;Span 父子嵌套关系;标识符全部放在 attributes,Span name 固定;默认埋点禁止携带 Prompt、工具参数、返回内容、密钥等敏感数据。
生命周期:prompt()/ resume()接收成功之后才开启操作 span;resume 每次重新生成 span,携带 recovery:true;进程崩溃 span 不会做清理;下次 resume 新建 span。Trace 上下文不会持久化到记录 / 条目。
19. 测试策略
划分三层测试:Tier‑A、Tier‑B、Tier‑C,各司其职。
- Tier‑A:化简逻辑与恢复测试
预先向 Session 写入 Crash 位点对应的记录、条目,调用 resume,校验最终持久化输出;覆盖全部崩溃位点、工具重放、队列、延迟句柄、重试上限、溢出、导航。内存、JSONL、SQLite 后端做一致性校验。 - Tier‑B:写入轨迹一致性
运行完整 Harness,拦截全部 E/R/L/G/H,校验输出顺序与文档轨迹完全一致;校验追加‑only 上下文不变性。 - Tier‑C:手动驱动模式确定性交织测试
使用 drive:"manual";在任意动作位点 close () 模拟崩溃,重新 open + resume;同时测试竞态表中每一行的两种执行时序。
附加测试套件:埋点一致性测试、钩子事件测试、计费账本完整性测试、v3 兼容 fixture 测试、原有 agent‑loop 兼容性回归。
20. 实现状态与工作包划分
限定修改包范围:packages/agent、packages/session‑backends/sqlite‑node、packages/telemetry、packages/ai请求选项层。不改动 packages/coding‑agent;v3 兼容只保证仓库可以读取 v3 文件。
任务包划分规则、认领流程;分为多条开发轨道:
-
Track F — 脚手架基础 -
Track QA — 遗留测试整理 -
Track R — 查询、化简、恢复 restore -
Track J — JSONL 存储后端 -
Track I — 底层原语:埋点、钩子、事件、变更队列、Effects、manual gate -
Track L — agent‑loop 重构拆分 -
Track H — Harness 上层集成,Run 完整运行链路 -
Track C/N — 结构化操作:压缩、导航 -
Track O — 可观测性收尾、快照事件、埋点插桩、审计、后端对等校验
依赖关系、合并顺序,规避对 agent‑harness.ts的并行冲突修改。
21. 必读参考文件列表
列出代码仓库内参考源文件,实现前阅读顺序。

