大数跨境

AgentHarness:具备故障恢复能力的 AI 对话运行框架设计

AgentHarness:具备故障恢复能力的 AI 对话运行框架设计 苏哲管理咨询
2026-09-15
9
导读:Durable AgentHarness 是具备崩溃恢复能力的 AI 会话运行框架,仅兼容读取旧版 coding‑agent v3 JSONL 会话,不做数据迁移脚本。会话由对话树、执行通道 Lane

编者摘要: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/harnesspackages/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. 会话定义

会话是具备持久化能力的状态集合,由四部分构成:

  1. 对话树(Tree)
    完整对话内容。由携带 parentId关联关系的条目构成,包含消息、模型 / 思考 / 工具调用变更、压缩摘要、分支摘要、自定义条目。对话树为共享被动数据,不归属于任意 Lane,只做追加写入,条目永远不会被修改或删除。
  2. 执行通道(Lanes)
    实际业务执行载体。每条 Lane 包含名称与叶子位点:即后续工作将要接续扩展的条目。每个会话默认内置 main通道;应用可基于外部业务标识(Slack 线程 ID、邮件线程 ID)创建更多 Lane。
  3. Lane 操作日志
    记录发生的事件与待执行任务。每条 Lane 维护一份时序扁平记录序列:操作启动、步骤尝试、工具启动、消息入队、操作完成。持久化能力依托该日志实现:进程崩溃后,新进程读取日志,继续完成 Lane 的任务。正常执行流程不会读取该日志。
  4. 全局事实(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 的每一次动作,要么生成挂载在自身叶子后的对话条目,要么写入自身专属操作日志。

不变性约束

  1. 对话树仅存储对话内容,不存放 Lane 状态、编排状态、各类指针。
  2. 条目的父链永远不可变更;分支共享历史前缀,不会复制拷贝条目。
  3. Lane 的叶子位点只有两种变更途径:Lane 追加新条目,叶子指向该条目;或者执行导航操作,直接跳转至某个已有条目。
  4. 操作日志记录不会修改对话树。即便删除全部操作日志,对话依然完整有效。
  5. 单条 Lane 最多只能存在一条未完成操作;一条 Lane 出现两条未完成操作判定为数据损坏。
  6. 条目是会话共享资源;日志记录归属于唯一 Lane。多条 Lane 的执行路径可以复用同一条条目;一条记录只能属于某一条 Lane。
  7. 日志记录不等同于对话条目:记录描述执行过程,不属于对话内容,不会进入模型上下文、转录文本、分支查询、分支拷贝。单条 Lane 内部,记录时序本身就表达顺序,无需 parent 关联。

3. 执行通道(Lane)

Lane 是对话树上的命名位点,绑定序列化排队的业务任务。类比 Git 工作树检出分支:命名绑定一个提交点;新工作推进该位点;可以跳转至任意已有条目,不修改历史记录;同一分支不会被重复检出。与 Git 的区别:导航可以跳转到历史任意条目,不限于向前移动。

每个会话自带 main通道。应用指定锚点条目与名称即可创建新 Lane;Lane 名称为业务永久键值,如 Slack 线程 ID。UI 不会直接罗列抽象 Lane;由平台线程列表承担该展示能力。

Lane 持有的资源

  • 叶子位点(leaf)
    新条目会挂载到此位点,更新叶子;导航操作直接跳转叶子。
  • 操作日志
    最多一条处于打开状态的操作。Lane 繁忙时,收到第二条操作请求直接拒绝,不影响其他 Lane。
  • 各类队列
    重定向指令、后续跟进消息、下一轮运行消息,全部归属对应 Lane。
  • 配置视图
    模型、思考等级、激活工具配置,读取 Lane 叶子路径上的历史配置条目。多条 Lane 可以使用不同模型,互不感知。工具实现、资源、流选项属于 Harness 全局配置;仅工具激活状态按 Lane 隔离。

运行规则

  1. 多条 Lane 可并行执行操作;Harness 依然维持单写入者,不同 Lane 的记录、条目会交错写入共享序列号序列。
  2. 创建 Lane 不会复制任何数据;Lane 不支持删除、重命名。
  3. Lane 内部状态变更操作,会在该 Lane 的变更队列上线性执行:校验、最多一次持久化写入、内存状态更新全部完成,才会处理下一次变更。服务商请求、工具调用、钩子、重试逻辑不在变更队列内执行。
  4. 两条 Lane 当前叶子位点相同时,下一次追加就会自然分叉,由对话树处理分支逻辑,Lane 之间无需协同。
  5. 存在未完成操作的 Lane,恢复后处于挂起状态,和其他 Lane 相互独立。挂起原因分为进程崩溃、服务商延迟请求(详见第 1 章节)。

4. 任务如何执行

操作(Operation):Lane 上持久化工作单元

分为三类:

  1. 运行(Run)
    已接收的提示词,包含全部自动接续逻辑:工具调用、指令重定向、后续消息、自动上下文压缩;无待处理任务代表运行结束。
  2. 压缩(Compaction)
    用一条摘要条目替换旧上下文。
  3. 导航(Navigation)
    将 Lane 叶子跳转至已有条目,可选择性生成分支摘要。

操作需要先被 “接收” 才开始执行;接收动作本身会持久化。崩溃恢复后,已接收操作要么被恢复完成,要么被显式关闭。每条 Run 最终状态只能是 completedfailedaborted;压缩、导航额外存在 declined,代表钩子在实际生效前否决该结构化操作。

运行、轮次、步骤

  • Run(运行)
    由若干轮次(turn)组成。
  • Turn(轮次)
    一次助手回复 + 该回复触发的完整批量工具调用。
  • Step(步骤)
    操作内部可重试的最小单元:生成助手消息、生成压缩摘要、生成分支摘要。一个步骤可以发起 0 次、1 次或多次服务商请求。失败的尝试会重试同一个步骤;尝试次数会持久化,重启进程不会重置计数。

延迟服务商请求会终止助手步骤:持久化携带句柄的助手消息,步骤关闭,操作整体挂起;后续兑现句柄再追加真实返回结果(详见第 1 章节)。

每一条发起实际效果的工具调用同样是一个步骤。tool_started标记启动;工具结果条目完成关闭。批量工具可以同时开启多个工具步骤;并发执行,最终结果按照原始输入顺序落盘(详见第 14 章节)。

队列与延迟写入

两种向运行中 Lane 投递输入的机制,中止行为存在差异:

  1. 队列
    承载对话意图:steer修改当前任务、followUp在模型停止后追加任务、nextRun为下一次运行预置消息。steerfollowUp在中止操作时会丢弃,载荷返还调用方;nextRun消息可以留存。
  2. 延迟写入
    承载事实数据:步骤执行过程中收到的条目、配置变更。中止操作不会丢失,即使取消任务依然会落地执行。

两者在接收阶段就完成持久化:接收调用向 Lane 操作日志写入完整载荷记录,调用才返回。对话条目不会立刻写入;等到消费 / 应用该数据时才追加。进程崩溃发生在接收和条目写入中间,恢复逻辑读取记录完成追加。已接收输入不会丢失。

检查点(Checkpoint)

每一轮次结束,Lane 抵达一个检查点,执行逻辑:

  1. 落地全部待处理延迟写入;
  2. 消费队列内的重定向消息;
  3. 如果下一次请求会超出上下文窗口,则执行压缩。

压缩也存在被动触发场景:服务商返回结果提示上下文超限(溢出报错、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 的条目载荷;parentIdseqtimestamp由存储层追加条目时分配,挂载到 Lane 当前叶子位点。

完整记录类型、TS 接口定义参考原文文档。

关键说明:

  1. 被拦截、校验失败的工具调用不会生成 tool_started记录;不会产生实际效果,因此无需意图记录;会直接生成带错误标记的工具结果条目。崩溃发生在写入该条目之前,该决策会丢失;恢复阶段会再次运行 before_tool
  2. 工具步骤不需要单独的结果记录;工具结果条目就是完整持久化输出,包含批量控制标记 terminate。执行完毕但还没写入结果条目就崩溃,会按照重放策略处理(第 6 章节);恢复会重新运行 after_tool,该行为在第 1 章节非目标中已明确允许。
  3. Token 计费是特例:计费完整性不能依赖结果条目。重试步骤会产生失败尝试,这些尝试不会生成对话条目,但消耗的 Token 不能丢失。因此每一次服务商请求完成,优先写入 usage 计费记录,之后再做分类、重试判断、丢弃响应。工具、钩子上报消耗也会生成对应记录。应用可以写入 adjustment类型记录用于无法自动统计的场景。
  4. 条目内部 usage字段是响应的不可变快照,写入后不再修改;读取时的真实消耗 = 查询所有绑定该 entryId 的 usage 记录(原始记录 + 调整记录总和);会话总消耗为全部 usage 记录求和。恢复阶段允许出现重复计费:重试步骤、重放工具都会各生成一条计费记录。

恢复判定为数据损坏的场景

  • Lane 存在多条打开操作;
  • 记录引用不存在或者已经结束的操作;
  • 同一个步骤内部尝试编号不连续;
  • 压缩尝试缺少 compactionReason,其他步骤携带该字段;
  • Run 已经收到 abort_requested,后续还出现属于该 Run 的 steer/followUp 入队记录;
  • queue_cancelled
    指向不存在入队记录,或者对应条目已经生成;
  • 同一个结构化步骤多次尝试,resultEntryIdcompactionReason前后不一致;
  • 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 的恢复仅读取两份有界数据:

  1. 自该操作启动之后,本 Lane 的全部记录;之前旧记录无需读取。
  2. 该操作新增生成的全部条目(从叶子回溯到操作锚点 sourceLeafId)。

额外做少量点查询:预分配 ID 条目、锚点位置的配置信息(模型、思考等级、激活工具)。全部为索引查询,不扫描完整会话历史、不扫描其他 Lane。

空闲 Lane 的剩余状态为未消费 nextRun队列。

规约化简(Reduction)

基于上面两份读取结果,推导 Lane 当前全部内存状态:是否正在中止、已使用的尝试次数、未完成步骤、工具批量状态、延迟句柄、待消费队列、待落地延迟写入、缺失初始消息、结构化操作目标等。

usage计费记录不参与编排逻辑化简,只做统计。

正常运行过程,Harness 每写一条数据同步更新内存状态;恢复阶段完全从存储重新计算得到状态。内存状态是日志与条目的化简产物,二者不能存在矛盾。

Resume(恢复继续执行)

resume()根据化简出来的状态继续运行未完成操作,执行顺序:

  1. 补全缺失初始消息;
  2. 如果标记中止,执行调和逻辑,生成合成工具结果、关闭助手消息,标记操作中止结束;
  3. 处理未完成工具批量;
  4. 如果存在未兑现延迟句柄,执行兑现逻辑;
  5. 如果检测到终端失败(助手错误消息):落地全部已接收写入、消费队列消息;如果消费队列没有产生新任务,直接标记 Run 失败结束;恢复逻辑不会自动修复这类运行;
  6. 存在未完成步骤:继续该步骤;达到重试上限则标记操作失败;压缩步骤会沿用记录里的 compactionReason
  7. 其他情况:跳转至下一个检查点,正常应用待处理写入与队列。

恢复阶段追加条目有一条特殊规则:如果预分配 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.okResult.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)

事件流是扁平一维序列。 约束:

  1. 事件监听回调抛出异常会被捕获,生成 handler_error事件,不会中断主流程;
  2. 事件投递时序与进程执行顺序一致;跨 Lane 不保证严格按照存储 seq 顺序;持久化消费者请直接读取日志接口;
  3. 事件不持久化,不会重放。重连必须重新执行 watch;
  4. 代表持久化变更的事件,触发时机为数据提交完成之后;事件看到的数据已经可以通过查询接口读到;
  5. 事件返回转换之后的最终值(钩子执行完毕);
  6. 全部载荷 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 压缩条目为止,经过 entryProjectorstoProviderMessages转换得到发给服务商的消息。

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 在这些原语外层包裹持久化写入边界。

  1. streamAssistant
    单次助手服务商请求;
  2. 工具调用三阶段:prepareToolCall(校验、before_tool)→ executeToolCall(实际执行)→ finalizeToolCall(after_tool 处理结果);
  3. 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封装,挂起动作,缓存待执行动作;提供 peekActionexecuteActionrunToCompletion

重入支持:一个动作执行过程中又调用 fx,新动作继续入队;manual 模式下会逐个释放。Lane 上层接口不受 gated 影响;暂停过程依然可以调用 abort、steer,会立刻进入变更队列执行。 close()挂起状态会拒绝本地 Promise;持久化操作保持原样,重新打开会话 resume 即可继续。

LaneState 内存状态

Lane 运行时内存编排状态,必须等价于化简函数 reduceLaneState的输出。打开会话 restore 通过日志条目调用化简函数得到;正常运行每一次提交同步更新。

过程内部信号

使用内部异常流转控制流:RunFailedParkAbortedOverflow;异常不会向外抛出给调用方;只有存储损坏等严重错误向上抛出,导致 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 接口增加可选的 fetchDeferredcancelDeferred。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参数传递。包划分:

  1. packages/telemetry
    通用契约、内存参考实现、无空实现、schema 基础工具;
  2. packages/agent/src/harness/telemetry.ts
    定义 Harness 与 AI 请求两套领域 schema;
  3. 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/agentpackages/session‑backends/sqlite‑nodepackages/telemetrypackages/ai请求选项层。不改动 packages/coding‑agent;v3 兼容只保证仓库可以读取 v3 文件。

任务包划分规则、认领流程;分为多条开发轨道:

  1. Track F — 脚手架基础
  2. Track QA — 遗留测试整理
  3. Track R — 查询、化简、恢复 restore
  4. Track J — JSONL 存储后端
  5. Track I — 底层原语:埋点、钩子、事件、变更队列、Effects、manual gate
  6. Track L — agent‑loop 重构拆分
  7. Track H — Harness 上层集成,Run 完整运行链路
  8. Track C/N — 结构化操作:压缩、导航
  9. Track O — 可观测性收尾、快照事件、埋点插桩、审计、后端对等校验

依赖关系、合并顺序,规避对 agent‑harness.ts的并行冲突修改。

21. 必读参考文件列表

列出代码仓库内参考源文件,实现前阅读顺序。

【声明】内容源于网络
0
0
苏哲管理咨询
为企业及组织提供AI+战略、数智化转型咨询及观点、建议等
内容 2205
粉丝 0
苏哲管理咨询 为企业及组织提供AI+战略、数智化转型咨询及观点、建议等
总阅读45.8k
粉丝0
内容2.2k