Preview Bundle 协议与 Trajectory 系统
学习目标
- 理解
preview-bundle/v1协议的实现 - 掌握三层持久化模型的工程实现
- 了解 Session 文件锁定的
_locked_save_json模式 - 掌握
trajectory.json的 append-only 设计与回放机制
前置知识
本章涉及 Agent 工作流 Preview 的通用模式,建议先阅读:
下文直接聚焦 CLI-Anything 的具体实现。
项目实践
preview_bundle.py 协议实现
preview_bundle.py(~500 行)是所有 Harness 共享的 Preview Bundle 协议实现。
协议版本:preview-bundle/v1
Bundle 目录结构:
bundle_003/├── manifest.json # 元数据:时间戳、命令、源指纹├── summary.json # 摘要:命令描述、关键属性├── render.png # 渲染产物└── artifacts/ # 额外产物三层持久化实现
| 对象 | 格式 | 可变性 | 用途 |
|---|---|---|---|
bundle_dir | 目录 | 不可变 | 每次 capture 的快照 |
session.json | JSON 文件 | 可变 | Live Preview 的当前头 |
trajectory.json | JSON 数组文件 | 追加式 | 命令 → Preview 的完整历史 |
关键设计:_bundle_dir 不是永久的构建身份。它只是单点快照路径,会随源指纹变化而改变。稳定的回放身份锚定在 session_dir + trajectory.json。
Publish 流程
每条 trajectory 条目包含:
- 命令描述
- 时间戳
- Bundle 路径
- 源指纹(源文件哈希)
- 发布原因(mutation / capture / start)
Session 文件锁定(源码验证 guides/session-locking.md)
并发写入 session JSON 可能导致损坏。CLI-Anything 使用排他文件锁定:
# 伪代码:_locked_save_json 模式import fcntl
def _locked_save_json(path: str, data: dict): with open(path, "r+") as f: fcntl.flock(f, fcntl.LOCK_EX) # 排他锁 f.seek(0) f.truncate() # 截断旧内容 json.dump(data, f, indent=2) # 写入新内容 fcntl.flock(f, fcntl.LOCK_UN) # 释放锁关键点:
- 使用
"r+"模式打开(需要文件已存在) ftruncate()在锁内执行——防止其他进程在截断和写入之间读取到空文件- 整个操作在锁内完成
preview live status --json 紧凑设计
为了避免 Agent 每次状态探测都读取整个 trajectory.json(可能包含数百条条目),preview live status --json 返回紧凑的 trajectory_summary:
{ "active": true, "session_dir": "/path/to/session_001", "trajectory_summary": { "total_commands": 42, "latest": { "command": "chassis build", "bundle": "bundle_007", "timestamp": "2026-05-31T10:45:00Z" } }}Agent-Cheap 设计:Agent 可以高频调用此命令而不会浪费 Token——返回的是摘要而非完整历史。
回放机制
trajectory.json 的 append-only 性质使完整回放成为可能:
- 读取 trajectory.json 的所有条目
- 按时间顺序重放每个命令
- 每个命令对应一个 Bundle,可以查看中间状态
- 完整重现 Agent 的工作轨迹
这对于调试、教学展示和审计都有价值。
Preview 协议的 CLI 表面
Harness 如果支持 Preview,应至少暴露:
| 命令 | 用途 |
|---|---|
preview recipes | 列出可用 Preview 配方 |
preview capture | 新鲜渲染/导出,发布新 Bundle |
preview latest | 返回最新已有 Bundle |
preview diff | 发布不可变对比 Bundle |
preview live start/push/status/stop | Live Preview 生命周期 |
Producer/Consumer 协议回顾
CLI-Anything 严格分离角色:
- Producer(
cli-anything-<software> preview ...):渲染、发布 - Consumer(
cli-hub previews ...):读取、展示
Consumer 命令:
cli-hub previews inspect <session>— 查看 Bundle 内容cli-hub previews html <session>— 生成 HTML 展示cli-hub previews watch <session>— 实时监控新 Bundlecli-hub previews open <session>— 在浏览器中打开
问题与规避
| 问题 | 规避策略 |
|---|---|
| 并发写入损坏 session.json | _locked_save_json 排他文件锁 |
_bundle_dir 被当作永久身份 | 文档强调稳定回放锚定 session_dir + trajectory.json |
| trajectory.json 增长过大 | trajectory_summary 提供紧凑摘要 |
| 源指纹变化后旧 Bundle 引用失效 | 源指纹变化时强制重新 capture |
| Bundle 目录名冲突 | 自动递增编号(bundle_001, bundle_002, …) |
设计取舍
Append-only vs 可编辑 Trajectory
选择:trajectory.json 只追加,不修改。
原因:
- 完整的历史审计链
- 回放的可重复性
- 避免历史被篡改导致不一致
代价:文件随时间增长。但 trajectory.json 是 JSON 数组,可以压缩或归档旧条目。
flock vs 其他锁定机制
选择:fcntl.flock() 排他文件锁。
原因:
- POSIX 标准,Linux/macOS 广泛支持
- 比临时文件+原子替换更简单
- 不需要外部锁服务
代价:Windows 不支持 fcntl。CLI-Anything 主要在 Linux 环境运行(CI 和服务器),Windows 兼容性通过 Git for Windows 的 bash 层处理。
参考来源
- 源码验证:
preview_bundle.py(~500 行) - 源码验证:
guides/session-locking.md - 源码验证:
guides/preview-methodology.md - 源码验证:FreeCAD/Blender Harness 中的 preview 命令