8 min read教程

PiAgent 10|会话树:JSONL 如何支持恢复与分支

从 parentId、当前 leaf 到回退和分叉,理解 Pi 为什么不用“可变消息数组”保存 Agent 历史

PiAgent 10|会话树:JSONL 如何支持恢复与分支#

parentId、当前 leaf 到回退和分叉,理解 Pi 为什么不用“可变消息数组”保存 Agent 历史

如果会话只是一个数组,回退就意味着删除后面的消息;删除会让调试、审计和恢复失去证据。Pi 把会话保存成带父子关系的 JSONL 树:回退只移动当前 leaf,从旧节点继续工作自然产生新分支,旧路径仍然保留。

1. 文件里保存的不是只有消息#

会话文件通常包含一个 header 和多个 entry:

text
header
  └─ model_change
      └─ user message
          └─ assistant tool call
              └─ tool result
                  └─ assistant answer

entry 至少需要自己的 ID、parentId、时间戳和类型。消息只是其中一种类型,其他类型还可以表达模型变化、压缩摘要、分支摘要、标签和扩展自定义数据。

这种设计解决了两个问题:历史结构不会被“重新拼字符串”破坏,恢复时也不需要猜某一行到底代表用户消息还是运行元数据。

2. 追加、回退与分支#

追加

新 entry 的 parentId 指向当前 leaf,旧 entry 不修改。追加是最常见的操作,也最容易保证不可变历史。

回退

回退把活动 leaf 移到较早节点,但不删除后续节点。当前上下文只沿新的 leaf 向父节点回溯,旧分支仍可查看。

分支

在历史节点上继续输入,会创建新的子节点。它不是复制整个文件,而是在同一棵树上产生另一条路径:

text
root → A → B → C       原路径
          └→ D → E     从 B 继续的新分支

3. 当前上下文是 leaf 的祖先路径#

恢复会话时,不能把 JSONL 所有行按文件顺序全部发送给模型。正确过程是:

  1. 找到当前 leaf。
  2. 沿 parentId 向上收集祖先。
  3. 反转为从 root 到 leaf 的顺序。
  4. 过滤不进入 LLM 的元数据和 UI 消息。
  5. 交给消息转换和 Provider 适配层。
text
文件事实:所有分支都存在
当前上下文:只有当前 leaf 的祖先路径可见

这一区分是分支功能正确性的核心。历史存在不代表当前模型可见。

4. SessionManager 的职责#

SessionManager 负责会话文件和树结构,不负责模型推理。它通常提供:

  • 创建或打开 JSONL 会话。
  • 追加 entry 与移动当前 leaf。
  • 从路径恢复和遍历树。
  • 创建分支、添加标签和记录模型变化。
  • 写入压缩摘要与分支摘要。
  • 在内存模式下提供相同的会话接口。

AgentSession 将 Agent 的消息生命周期接到 SessionManager:消息结束后,必要的 user、assistant 和 tool result 被持久化;UI 状态、瞬时进度和监听器本身不应被当作永久消息写入。

5. 为什么 JSONL 适合 Agent#

JSONL 的特点是“一行一个 entry,追加友好”:

  • 崩溃前已经写入的历史更容易保留。
  • 可以流式读取,不必一次解析整个巨大 JSON 数组。
  • 每行是结构化对象,便于诊断和迁移。
  • 父子 ID 让分支不依赖数组下标。

它并不自动保证强一致性。生产环境还需要处理半行写入、重复 entry、文件锁、并发追加、损坏恢复和版本迁移。

6. 本地 CLI 与 Web 服务的不同默认值#

本地 Coding Agent 通常按工作目录隔离会话,默认路径类似:

text
~/.pi/agent/sessions/<encoded-cwd>/<session>.jsonl

这适合单用户、单机和人工调试。Web 服务则需要:

  • 用户 ID 与 session ID 的双重隔离。
  • 数据库或对象存储中的受控持久化。
  • 并发请求的乐观锁或队列。
  • 访问权限和删除策略。
  • 不把服务器当前工作目录当作租户边界。

短期无状态服务可以使用 SessionManager.inMemory(),但这意味着进程重启会丢失历史。要恢复对话,就必须在消息或 entry 层把必要状态写入自己的存储。

7. 恢复不是重新 prompt 一遍#

一个可靠的恢复流程应该让模型看到与上次一致的工作上下文,而不是把用户最后一句话再发一次。至少要恢复:

  • 当前分支路径。
  • 模型和 thinking level 变化。
  • 压缩摘要及其保留边界。
  • 必要的工具结果。
  • 扩展需要的自定义状态。

恢复后仍要重新装配当前的工具和资源,并重新验证权限。历史记录可以证明过去发生了什么,但不能自动授予现在仍然有效的权限。

常见误区

  • 回退时物理删除后续 entry,失去调试证据。
  • 将所有 JSONL 行都发送给模型,混入其他分支和 UI 元数据。
  • 用数组下标配对 entry,插入或分支后关系失效。
  • 认为 SessionManager 会自动解决多用户隔离。
  • 恢复旧会话时沿用旧权限,不重新检查当前身份和工具策略。

小结

会话树把 Agent 历史从“可变数组”变成“可导航的事实图”:追加只增加节点,回退只改变活动 leaf,分支不破坏旧路径,当前上下文由祖先路径投影得到。JSONL 负责保存结构,SessionManager 负责导航,AgentSession 负责把运行消息接入这棵树。

源码定位

  • 会话管理:packages/coding-agent/src/core/session-manager.ts
  • Session 与持久化连接:packages/coding-agent/src/agent-session.ts
  • 会话格式说明:官方文档 session-format
  • 压缩与分支摘要:packages/coding-agent/src/

相关文章