7大开源Agent源码对比解读------会话管理
本文是「7大开源Agent源码对比解读」系列第 3 篇。评估对象:codex、gemini-cli、qwen-code、opencode、kimi-code、deepseek-harness(下称 dsh)、oh-my-pi(下称 omp)。所有机制描述附源码引证。
| 篇 | 文章 | 内容速览 |
|---|---|---|
| 总述 | 拆了七大开源 Agent 的源码,最高分竟然不是 Codex | 总体结论、评估方法与评分卡 |
| 01 | 架构对比 | 四种流派与分化根源、主循环与事件机制、插件化与耦合风险 |
| 02 | 上下文管理 | 压缩触发阈值、摘要方式、token 计数口径、跨会话记忆 |
| 03 | 会话管理 | 持久化模型三层次、并发控制、崩溃恢复与 resume / fork |
| 04 | 工具调用 | 注册与可见性、并行调度四种语义、审批门控、错误处理 |
| 05 | 重连与容错 | 重试预算、流中断处理、降级链、副作用安全 |
| 06 | 系统提示词与指令遵循 | 四种组装范式、动态注入、注入防御三层与共同敞口 |
| 07 | 思维链与工作流编排 | 思维链接入、plan 模式语义、子代理治理、工作流引擎 |
| 08 | 性能设计 | 前缀缓存三档分化、启动优化、成本核算 |
| 09 | 可扩展性 | MCP 接入、自定义工具、多 provider、SDK 与 API |
| 10 | 安全与权限控制 | 沙箱两种语义、审批默认姿态、企业管控、敏感数据保护 |
会话管理的核心问题是:会话以什么形态落盘,崩溃后怎么恢复,多个进程同时碰同一个会话怎么办。七个项目的持久化模型可以归入三个递进层次。
持久化模型的三个层次
- 快照式,代表 gemini-cli。把当前对话状态整体序列化成 JSON 文件,恢复就是读回最近快照。简单,但历史不可分叉、不可重放。
- 转录式,代表 codex、qwen-code、kimi-code、omp。会话写成追加式 JSONL 日志,每条记录一个事件,恢复就是从日志重放。已经能 resume 和 fork,但状态投影仍需调用方现场重建。
- 事件溯源式,代表 dsh、opencode。日志即唯一真相,所有可读状态(消息、部件、UI)都是日志的投影。天然支持审计、重放、双后端,代价是要维护投影机和事件 schema 版本。
持久化格式与并发控制
| 项目 | 持久化格式 | 并发控制 |
|---|---|---|
| codex | JSONL rollout 按日期分目录 + SQLite 索引 + zstd 冷压缩 | 进程内单写者任务 + 跨进程文件锁 |
| gemini-cli | JSON 记录 + 影子 git 快照(默认关闭) | 未发现会话写锁 |
| qwen-code | JSONL 转录 + 检查点 | session-writer-lease 文件锁租约 |
| opencode | SQLite WAL(session/message/part 表) | 每 session 一个协调器 + BusyError |
| kimi-code | wire.jsonl + state.json + 自研 minidb | 串行 flush + O_APPEND,无跨进程锁 |
| dsh | append-only 事件日志,JSONL(zstd)/SQLite 双后端 | 协调器 LRU + 活会话快照等待 |
| omp | JSONL 树(每条带 parentId)+ blob 内容寻址仓 | AgentBusyError 禁并发 prompt |
恢复与容错设计
| 项目 | resume / fork | 崩溃恢复设计 |
|---|---|---|
| codex | resume、fork、revert,fork 记录血缘 | revert 保线程 ID 新建不可变文件 |
| gemini-cli | --resume + /rewind + checkpoint | 磁盘满时降级提示,禁用录制不崩溃 |
| qwen-code | --continue、--resume、--fork、/restore | 四类恢复计划 + 孤儿 tool_use 修复 |
| opencode | --continue、--fork、--replay、share | 无状态循环重推导 + 中断工具标记 |
| kimi-code | --continue、fork(不继承 goal) | 记录重放,只重建内存状态 |
| dsh | load、inspect、fork、seed | 格式版本拒绝 + crash 补合成 turn/end |
| omp | fork 只移指针 + 导入 Claude/Codex 会话 | 终端 breadcrumb + continueRecent |
逐家看点
codex:JSONL 为体,SQLite 为用
每次会话写成一个 rollout-*.jsonl,按日期分目录存放,文件名本身编码时间戳和线程身份(rollout/src/rollout_file_name.rs:39-47)。JSONL 是规范真相,SQLite 只是查询索引,索引坏了可以由 rollout 重建。
冷数据治理做得很细。后台 worker 把 7 天未修改的 rollout 压成 zstd:先写临时文件、回读校验、再确认原文件未变才落地(compression.rs:632-701)。压缩前还要查 RolloutReferenceIndex,被 fork 引用的 rollout 一律跳过,避免压缩切断 fork 链。
写入走 mpsc 通道,唯一的后台任务串行落盘。后台任务一旦以 IO 错误退出,错误被存住,之后所有写操作原样复现同一个失败(terminal_failure),不做无谓重试。
revert 的语义是「保留前缀、另起新 rollout」:线程 ID 不变,新建一个不可变文件引用保留的前缀,唯一可变的切换点是 SQLite 里的路径指针(thread-store/src/local/revert_thread.rs:15-18)。旧 rollout 原封不动。
gemini-cli:影子 git 三位一体回滚
gemini-cli 独有的「文件 + 对话 + 工具调用」三位一体回滚:每次可回滚的工具调用前,在影子 git 仓库做一次文件快照,连同对话历史和工具调用参数一起存成 checkpoint(checkpointUtils.ts:130-139)。/rewind 时 git checkout 回该 commit 并重放对话。
影子仓库完全脱离用户真实 git 环境:重置 GIT_CONFIG_GLOBAL、GIT_DIR 等全部环境变量,专用 identity,禁用 gpg(gitService.ts:102-142)。
默认关闭(checkpointing.enabled default false)。建仓库加每工具一次 commit 确实有成本,但对编辑类工具来说,这是七者中唯一能同时回滚文件和对话的方案,默认关掉有点可惜。
gemini-cli 没有会话级写锁,多开同一会话靠目录隔离,不防并发写同一转录文件。
qwen-code:唯一的多进程写租约
daemon 和 TUI 可能同时持有同一会话,qwen 用一个租约文件保证同一时刻只有一个权威写者。这是七个项目里唯一一套面向「多进程写同一会话转录」的工程级方案,细节密度很高:
- 锁记录带进程启动身份。 仅靠
isProcessAlive(pid)判断锁主是否活着会被 PID 复用骗,锁里存process_start_identity(Linux 读/proc/<pid>/stat启动 ticks + boot_id,macOS 用ps -o lstart=,Windows 用 PowerShell 取 StartTime.Ticks),身份不一致即判过期可回收(session-writer-lease.ts:270-312)。 - 防符号链接替换。 打开转录文件强制带
O_NOFOLLOW,打开前lstat确认是普通文件,防止攻击者把转录路径换成指向别处的符号链接。 - 转录哈希校验。 获取租约时把整条转录按 1MB 块算 SHA-256,此后每次追加前重新核对,租约之外被改动就抛错。
- 获取锁用硬链接的 create-new 语义。 写临时文件再
link到锁路径,EEXIST即被抢占。
恢复侧是 buildSessionRecoveryPlan:resume 时按转录尾部形态分成四类修复计划(session-recovery.ts:22-26)。
| 计划类型 | 触发条件 | 处理 |
|---|---|---|
| clean | 无中断 | 直接返回 |
| interrupted_prompt | 尾部是用户输入,模型未回完 | 重发用户输入,可自动续 |
| interrupted_turn | 有 functionCall 但无对应响应 | 合成失败工具结果补全,需确认 |
| degraded_history | 检测到父链缺失 | 禁止自动续,提示历史不完整 |
「孤儿 tool_use」是公开痛点:转录里有 functionCall 却没有配对的 functionResponse,直接喂给 provider 会报错。qwen 为每个悬空调用合成一个错误响应,把历史闭合成合法续接点(session-recovery.ts:206-209)。
opencode:一切进 SQLite,循环无状态
会话状态全进一个 SQLite 库,开 journal_mode = WAL,消息拆成 message 与 part 两层,part 是流式部件(文本块、工具调用块)。WAL 让多进程读写同一库成为可能。ID 是带前缀的 ULID(ses_、msg_、prt_),按字典序就是时间序,还能从 ID 反推创建时间。
主循环本身无状态:状态全在库里,每轮从库重读重推导。恢复就是从库重放事件投影出当前状态,不需要单独的 replay 模式。
并发控制是「每个 session 一个协调器」:同一 key 已有在跑的执行就加入它,不同 session 可并发(run-coordinator.ts:5)。已有活跃 turn 时新 prompt 被拒为 PromptConflictError,客户端落入队列重试。
中断处理学 qwen 的思路:工具执行中被打断时,failUnsettledTools 把 pending/running 的工具标为失败并附「Tool execution interrupted」,保证历史里 tool_use 永远有配对结果(runner/llm.ts:306-316)。
kimi-code:自研 KV 与重放契约
会话事件流写进 wire.jsonl,会话级状态快照另存 state.json。有意思的是自研存储引擎 minidb:buffered、append-only、group-commit 的 WAL(单写者,类 SQLite WAL),快照走临时文件加原子 rename 加 WAL 轮转(minidb/src/wal.ts:3-14)。
全文索引的分词器针对中文做了处理:CJK 连续段切成单字加相邻二字两类 token(如「会话」切成 会、会话、话),无须词典和分词器(tokenize.ts:3-13)。这是中文检索的经典做法,查询时同样分词后 AND 命中。
恢复契约是「只重建内存状态」:读 wire.jsonl 逐行重放,在内存重建上下文、工具、goal,不回写日志(agent/replay/)。fork 时显式丢弃 goal 文件(FORKED_SESSION_DROPPED_FILES = ['upcoming-goals.json']),新分支从零目标开始。
短板是 wire.jsonl 没有跨进程写锁,并发模型隐含「单一进程持有该会话」假设,多会话场景靠进程边界隔离。对照 qwen 的租约方案,这是可以补的一课。
dsh:不变量、双后端、宁可拒绝
dsh 的会话真相是一条只追加的事件日志,事件带单调 seq。在此之上有一条 repo 级运行时不变量:任何会到达模型请求的内容,必须先作为事件入日志,从而可从日志重建(packages/client/AGENTS.md:55)。纯展示数据不进日志,由 host 每帧重算。
#mermaid-svg-jXoCl5NCcbEd59JS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jXoCl5NCcbEd59JS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jXoCl5NCcbEd59JS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jXoCl5NCcbEd59JS .error-icon{fill:#552222;}#mermaid-svg-jXoCl5NCcbEd59JS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jXoCl5NCcbEd59JS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jXoCl5NCcbEd59JS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jXoCl5NCcbEd59JS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jXoCl5NCcbEd59JS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jXoCl5NCcbEd59JS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jXoCl5NCcbEd59JS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jXoCl5NCcbEd59JS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jXoCl5NCcbEd59JS .marker.cross{stroke:#333333;}#mermaid-svg-jXoCl5NCcbEd59JS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jXoCl5NCcbEd59JS p{margin:0;}#mermaid-svg-jXoCl5NCcbEd59JS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jXoCl5NCcbEd59JS .cluster-label text{fill:#333;}#mermaid-svg-jXoCl5NCcbEd59JS .cluster-label span{color:#333;}#mermaid-svg-jXoCl5NCcbEd59JS .cluster-label span p{background-color:transparent;}#mermaid-svg-jXoCl5NCcbEd59JS .label text,#mermaid-svg-jXoCl5NCcbEd59JS span{fill:#333;color:#333;}#mermaid-svg-jXoCl5NCcbEd59JS .node rect,#mermaid-svg-jXoCl5NCcbEd59JS .node circle,#mermaid-svg-jXoCl5NCcbEd59JS .node ellipse,#mermaid-svg-jXoCl5NCcbEd59JS .node polygon,#mermaid-svg-jXoCl5NCcbEd59JS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jXoCl5NCcbEd59JS .rough-node .label text,#mermaid-svg-jXoCl5NCcbEd59JS .node .label text,#mermaid-svg-jXoCl5NCcbEd59JS .image-shape .label,#mermaid-svg-jXoCl5NCcbEd59JS .icon-shape .label{text-anchor:middle;}#mermaid-svg-jXoCl5NCcbEd59JS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jXoCl5NCcbEd59JS .rough-node .label,#mermaid-svg-jXoCl5NCcbEd59JS .node .label,#mermaid-svg-jXoCl5NCcbEd59JS .image-shape .label,#mermaid-svg-jXoCl5NCcbEd59JS .icon-shape .label{text-align:center;}#mermaid-svg-jXoCl5NCcbEd59JS .node.clickable{cursor:pointer;}#mermaid-svg-jXoCl5NCcbEd59JS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jXoCl5NCcbEd59JS .arrowheadPath{fill:#333333;}#mermaid-svg-jXoCl5NCcbEd59JS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jXoCl5NCcbEd59JS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jXoCl5NCcbEd59JS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jXoCl5NCcbEd59JS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jXoCl5NCcbEd59JS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jXoCl5NCcbEd59JS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jXoCl5NCcbEd59JS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jXoCl5NCcbEd59JS .cluster text{fill:#333;}#mermaid-svg-jXoCl5NCcbEd59JS .cluster span{color:#333;}#mermaid-svg-jXoCl5NCcbEd59JS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jXoCl5NCcbEd59JS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jXoCl5NCcbEd59JS rect.text{fill:none;stroke-width:0;}#mermaid-svg-jXoCl5NCcbEd59JS .icon-shape,#mermaid-svg-jXoCl5NCcbEd59JS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jXoCl5NCcbEd59JS .icon-shape p,#mermaid-svg-jXoCl5NCcbEd59JS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jXoCl5NCcbEd59JS .icon-shape .label rect,#mermaid-svg-jXoCl5NCcbEd59JS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jXoCl5NCcbEd59JS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jXoCl5NCcbEd59JS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jXoCl5NCcbEd59JS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 折叠投影
append-only 事件日志(seq 单调 · JSONL(zstd) / SQLite 双后端)
turn/start
user 消息
assistant 消息
tool 结果
turn/end
平衡边界
模型可见上下文
UI 与存储
resume / fork / 审计 / 回放
运行时不变量:到达模型的内容
必须先入日志,且可从日志重建
双后端:同一份事件流可落地 JSONL(zstd) 或 SQLite。物理差异很实在,SQLite 能按 seq 定位只读后缀,顺序介质的 JSONL 只能整文件解析后跳到目标位置(session-persistence/src/index.ts:212-214)。
几条恢复语义值得抄:
- 格式版本拒绝先于解释。 后端遇到不认识的格式版本直接拒绝加载,宁可不可用也不冒险误读(session-persistence-jsonl/src/format.ts:234-243)。
- crash 补合成 turn/end。 冷加载时若最后一轮是被打断的完整 turn,持久地闭合它:补上缺失的工具错误和开着的边界,让日志结尾落在平衡的
turn/end。只读接口inspect则只在内存加合成闭合,物理撕裂尾不动。 - 活会话不做 crash 修复。 仍绑定活 Session 的身份,要么给出当前不可变快照,要么拒绝或等待,绝不与活写者争抢。
omp:树形会话,分支零成本
omp 的会话是一个 JSONL 文件,首行 header,其后每条 entry 都带 parentId,整体是一棵树。会话状态由一个可变的 leaf 指针选择当前活跃路径(session-manager.ts:434-439)。
fork 就是把 leaf 挪回某个祖先再 append:新 entry 从该祖先分叉出去,原分支一字不动地留在文件里。分支零成本、零拷贝、历史永不重写,这是七者中分支成本最低的设计。
#mermaid-svg-HJM7bfWPUIiz3Q5Z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .error-icon{fill:#552222;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .marker.cross{stroke:#333333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HJM7bfWPUIiz3Q5Z p{margin:0;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .cluster-label text{fill:#333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .cluster-label span{color:#333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .cluster-label span p{background-color:transparent;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .label text,#mermaid-svg-HJM7bfWPUIiz3Q5Z span{fill:#333;color:#333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .node rect,#mermaid-svg-HJM7bfWPUIiz3Q5Z .node circle,#mermaid-svg-HJM7bfWPUIiz3Q5Z .node ellipse,#mermaid-svg-HJM7bfWPUIiz3Q5Z .node polygon,#mermaid-svg-HJM7bfWPUIiz3Q5Z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .rough-node .label text,#mermaid-svg-HJM7bfWPUIiz3Q5Z .node .label text,#mermaid-svg-HJM7bfWPUIiz3Q5Z .image-shape .label,#mermaid-svg-HJM7bfWPUIiz3Q5Z .icon-shape .label{text-anchor:middle;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .rough-node .label,#mermaid-svg-HJM7bfWPUIiz3Q5Z .node .label,#mermaid-svg-HJM7bfWPUIiz3Q5Z .image-shape .label,#mermaid-svg-HJM7bfWPUIiz3Q5Z .icon-shape .label{text-align:center;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .node.clickable{cursor:pointer;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .arrowheadPath{fill:#333333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HJM7bfWPUIiz3Q5Z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HJM7bfWPUIiz3Q5Z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HJM7bfWPUIiz3Q5Z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .cluster text{fill:#333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .cluster span{color:#333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HJM7bfWPUIiz3Q5Z rect.text{fill:none;stroke-width:0;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .icon-shape,#mermaid-svg-HJM7bfWPUIiz3Q5Z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .icon-shape p,#mermaid-svg-HJM7bfWPUIiz3Q5Z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .icon-shape .label rect,#mermaid-svg-HJM7bfWPUIiz3Q5Z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HJM7bfWPUIiz3Q5Z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HJM7bfWPUIiz3Q5Z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HJM7bfWPUIiz3Q5Z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 沿路径构造上下文
setLeaf(B) 再 append D,
原分支 C 原样保留
会话 header(id、parentSession)
entry A
parentId=null
entry B
parentId=A
entry C
parentId=B(原 leaf)
entry D
parentId=B(新分支)
fork 前的 leaf 指针
fork 后的 leaf 指针
大块二进制(图像)不塞进 JSONL,外置到内容寻址的 blob 仓,按 sha256 存,天然幂等去重(blob-store.ts:26-32)。
omp 还能导入别家会话:把 Claude、Codex 的会话转成自己的树形 JSONL 继续(foreign-session-import 系列文件)。崩溃恢复靠终端 breadcrumb:创建会话时立即写一条面包屑,进程随后崩溃也能靠 continueRecent 找回。
给 Agent 开发者的借鉴清单
- 孤儿 tool_use 必须有标准处理。 崩溃恢复后,没有配对结果的工具调用会让 provider 拒绝整个请求。qwen 的合成错误响应和 opencode 的 interrupted 标记是两种成熟做法。
- 多进程会碰同一会话就上写租约。 qwen 的 lease 是完整样本:硬链接创建、进程启动身份防 PID 复用、
O_NOFOLLOW防符号链接、追加前哈希复核。 - 树形日志 + leaf 指针让分支免费。 比复制会话文件省得多,历史还不可变(omp)。
- 事件溯源让恢复语义变简单。 resume、fork、审计、回放都是同一份日志的投影(dsh、opencode)。
- 格式版本不兼容时宁可拒绝。 误读旧日志造成的数据损坏比「暂时打不开」严重得多(dsh)。
- 回滚要同时管文件和对话。 只回滚对话不回滚文件,用户看到的还是脏工作区(gemini-cli 影子 git 是唯一完整方案)。
- 冷数据压缩前检查引用。 被 fork 引用的历史不能压,否则分支断链(codex 的 RolloutReferenceIndex)。
文档地图
| 篇 | 文章 | 内容速览 |
|---|---|---|
| 总述 | 拆了七大开源 Agent 的源码,最高分竟然不是 Codex | 总体结论、评估方法与评分卡 |
| 01 | 架构对比 | 四种流派与分化根源、主循环与事件机制、插件化与耦合风险 |
| 02 | 上下文管理 | 压缩触发阈值、摘要方式、token 计数口径、跨会话记忆 |
| 03 | 会话管理 | 持久化模型三层次、并发控制、崩溃恢复与 resume / fork |
| 04 | 工具调用 | 注册与可见性、并行调度四种语义、审批门控、错误处理 |
| 05 | 重连与容错 | 重试预算、流中断处理、降级链、副作用安全 |
| 06 | 系统提示词与指令遵循 | 四种组装范式、动态注入、注入防御三层与共同敞口 |
| 07 | 思维链与工作流编排 | 思维链接入、plan 模式语义、子代理治理、工作流引擎 |
| 08 | 性能设计 | 前缀缓存三档分化、启动优化、成本核算 |
| 09 | 可扩展性 | MCP 接入、自定义工具、多 provider、SDK 与 API |
| 10 | 安全与权限控制 | 沙箱两种语义、审批默认姿态、企业管控、敏感数据保护 |