崩溃后自动复活:Unreal Agent append-only 会话存储与 Resume 恢复机制深度解析
【免费下载链接】unreal-agentAsync-first agent harness项目地址: https://gitcode.com/gh_mirrors/un/unreal-agent
Unreal Agent是 Unreal Labs 出品的一个异步优先(async-first)AI Agent 框架,它的 append-only 会话存储和 Resume 恢复机制能让 Agent 在进程崩溃、断电、输出中断之后"自动复活",从上次未完成的位置继续执行,而不是从零开始。本文用尽量少的代码,带你彻底看懂这套崩溃恢复设计是怎么做到的。
一、为什么 AI Agent 需要"崩溃恢复"?
传统的 AI Agent 进程一旦崩溃,所有中间状态——用户消息、模型回复、执行到一半的工具调用——全部丢失。下次运行只能从头再来,既浪费 token,又可能重复执行有副作用的操作(比如跑过一半的 shell 命令)。
Unreal Agent 的解法很直接:把会话当成一本只能往后写的账本。
💡 核心思想:会话历史是 append-only(仅追加)的持久化日志,任何时刻崩溃,重启后重放账本就能精确恢复到崩溃前的状态。
在 README 的项目术语表里,官方对 Session 的定义就是一句话:"append-only persisted history that can be forked"(可分叉的仅追加持久化历史),见 README.md。
二、append-only 会话文件:一行一条记录的 JSONL 账本
2.1 会话文件长什么样
每个会话对应一个以.session.jsonl结尾的本地文件,由 store.go 中的Store结构体管理。仓库里的测试黄金文件 golden-session.session.jsonl 展示了一条完整记录流:
| 行号 | 记录类型 | 含义 |
|---|---|---|
| 第 1 行 | session | 会话头:格式版本 + 会话 ID + 创建时间 |
| 第 2 行 | item | 外部输入(用户消息) |
| 第 3 行 | item | 新一轮(turn)开始 |
| 第 4 行 | item | 模型响应 |
| 第 5 行 | item | 工具调用状态 + 初始化的操作 |
| 第 6 行 | operation | 操作状态更新(awaiting → 后续状态) |
每条记录独占一行、以换行符结尾,这是刻意为之的——它是崩溃安全的基石。
2.2 追加写入的三步安全流程
真正的写入逻辑在 appendFile 函数里,只有三步,却环环相扣:
- 以
O_APPEND模式打开文件:操作系统保证多个追加者不会互相覆盖; Truncate(committedSize)丢弃残缺记录:如果上次写入在换行符之前崩溃,文件尾部会留下半行 JSON。重启后先截断到"最后一条完整记录"的位置,半行直接作废;Write+Sync:写完并强制刷盘,确保落盘才算数。
读取侧同样聪明:decodeLog 先用LastIndexByte找到最后一个换行符,committedSize之后的内容一律视为未提交数据——半行记录永远不会污染状态。
🔧 一句话总结:以"完整行"为提交边界 + 刷盘落盘 + 重启截断残行 = 任意时刻崩溃都不丢已提交数据、不产生脏数据。
三、Resume:重启后如何精确"复活"
崩溃后重新运行时,入口是 Store.Resume。它做两件事:
3.1 重放账本,重建内存状态
decodeLog逐行重放记录,把每条item和operation依次"回放"到内存状态机中。回放过程会严格校验:
- 记录序号必须连续递增(缺号立即报错);
- 格式版本不匹配(包括无法识别的旧版本 1)会显式拒绝恢复,而不是静默兼容;
- 每个操作的类型和版本号回放过程中不允许被篡改。
这套校验来自 validateOperation,保证重放出来的状态和崩溃前完全一致。
3.2 找出"没干完的活"
真正的复活逻辑在 resume() 里,它扫描整本账本,产出ResumeState(定义见 sessionstore.go),包含三部分:
| 字段 | 作用 |
|---|---|
Snapshot | 会话基本信息,用于确认身份 |
Operations | 所有未完成的操作——状态未到终态、或终态还没写进历史的操作都会被挑出来,供操作管理器重新调度 |
ExternalInputIDs | 已经接收但可能尚未处理完的外部输入 ID,用于恢复输入幂等去重 |
⚡ 举个例子:Agent 正在执行一个 shell 操作时断电。重启后
Resume发现该操作状态是awaiting(未完成),它就被放回Operations列表,由操作管理器用持久化的幂等数据(Idempotency)继续推进,而不会盲目前进。
3.3 输入幂等:重复投递不会造成重复执行
恢复不仅是恢复操作,还要恢复"哪些输入已经收过"。Unreal Agent 的 Inbox 是会话级、易失的去重层(见 inbox.go),崩溃后会清空;ExternalInputIDs正是把已接收的外部输入重新"注入" Inbox,这样上游重发的同一条消息在恢复后被安全丢弃,实现端到端幂等。
仓库中的集成测试 resume_test.go 验证了完整链路:输出中断 → 失败项已提交 → 重新运行 → 模型请求只发生一次且输入不重复,证明"复活"后不会重复烧 token。
四、Fork:同一本账本还能"分叉"
append-only 不只是为了恢复,还是分叉的基础。Fork 可以把某个会话在指定 turn 处的历史复制成全新会话,forkStoredState 负责找到分叉边界并继承历史;测试样例 golden-fork.session.jsonl 里可以看到fork类型记录完整记录了父会话和分叉点。
这意味着你可以让 Agent 从任意历史节点"岔路"探索不同方案,而原会话毫发无损——因为账本只增不改。
五、设计要点回顾:这套机制做对了什么?
- 提交边界清晰:一行一记录、换行符即提交点,崩溃最多丢"正在写的那半行",且会被自动截断;
- 状态可由日志完整重建:内存状态机不单独持久化,一切以账本为准,重放即可复活;
- 未完成工作显式化:
Resume精确挑出未终结的操作和待去重的输入,恢复=继续干,而不是重新开始; - 版本显式化:格式带版本号,不兼容时明确报错(见 README.md 中"An unsupported session version will always cause an explicit error on resume"),绝不悄悄猜;
- 可插拔:
Store是接口,localfile 只是本地实现,官方明确鼓励用代理实现把会话同步到远程沙箱等场景。
六、上手试试:体验崩溃恢复
用仓库自带的 runner 就能直观感受这套机制,详见 cmd/unreal-agent-runner/README.md:
go run ./cmd/unreal-agent-runner \ -workspace ./my-project \ -session-directory ./sessions \ -p 'Summarize this project.'指定-session-directory后,所有会话以 JSONL 账本形式落盘。执行过程中随时kill -9掉进程,再用相同 session_id 发起请求,Agent 就会从账本重放中恢复上下文并继续——这就是本文解析的"崩溃后自动复活"。
七、总结
Unreal Agent 的会话存储用最朴素的手段——仅追加的 JSONL 日志 + 换行符提交边界 + 全量重放——实现了工业级的崩溃恢复能力:不丢已提交数据、不重复执行操作、不重发已接收输入。对于正在构建自己的 AI Agent 系统的开发者来说,这套"账本式"设计(append-only 日志、幂等输入、显式未完成状态、版本化格式)几乎可以直接照搬。想深入细节,可以从 harness/sessionstore/localfile/store.go 和 harness/sessionstore/localfile/state.go 两个文件开始读起。
【免费下载链接】unreal-agentAsync-first agent harness项目地址: https://gitcode.com/gh_mirrors/un/unreal-agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考