Tape 与 context
本页解释 Bub 使用的 context 模型:每个 session 一条 append-only tape、标记重建点的 anchor、改变阶段的 handoff,以及把 tape entry 转成模型消息列表的 context selector。
更深入的模型见 tape.systems。本页只覆盖 Bub 的具体实现;需要完整理论时请查阅上游模型。
tape 原语
Section titled “tape 原语”Bub 复用了 tape 模型的四个原语:
- Tape — 单个 session 的事实序列,按时间排列。
- Entry — tape 上的一条不可变记录(message、tool call、tool result、event、anchor)。
- Anchor — 携带结构化 state 负载的检查点。可以从 anchor 重建 context,无需重扫整条 tape。
- View — 由 entry 派生的、面向具体任务的 context window。
恒守三条不变式:
- 历史是 append-only —— entry 永不被覆写。
- 派生物从不替换原始事实 —— 摘要是派生 view,而非编辑。
- context 是构造出来的,而不是整体继承 —— 每次 turn 都派生一份新的 view。
anchor 是重建标记
Section titled “anchor 是重建标记”anchor 不是 删除点。tape 完整保留 anchor 之前的所有内容;anchor 只告诉 context 构造从哪里开始重建。anchor 的 state 负载提供下一阶段所需的最小继承状态。
handoff 是带状态的阶段过渡
Section titled “handoff 是带状态的阶段过渡”handoff 是受约束的过渡:写入新 anchor、附上最小继承 state、把执行起点推过新 anchor。在 Bub 中,调用 handoff 时给出名字与可选 state dict;结果是同一条 tape 上新增一条 anchor entry。
Bub 的具体实现
Section titled “Bub 的具体实现”per-session tape
Section titled “per-session tape”每个 session 对应一条 tape。Bub 用 workspace 路径与 session id 计算 tape 名(Tape.session_tape):
workspace_hash = md5(str(workspace.resolve()).encode()).hexdigest()[:16]
session_hash = md5(session_id.encode()).hexdigest()[:16]
tape_name = f"{workspace_hash}__{session_hash}"
这意味着同一个 session_id 在不同 workspace 会得到不同 tape,两个仓库不会互相串 state。
默认的 provide_tape_store 返回根目录在 ~/.bub/tapes/ 的 FileTapeStore。插件通过提供自己的 provide_tape_store(例如 SQLite 或 HTTP 后端)来替换它。
spill sidecar
Section titled “spill sidecar”builtin spill 插件通过 provide_tape_sidecar 挂载 SpillStore、注册 spill.read,并通过现有 after_tool_call hook 限制大结果。sidecar 本身没有工具结果拦截约定。较大的结果存放在名为 <session-tape>__sidecar__spill 的 sibling tape 中。sidecar 与 session tape 使用同一个 TapeStore,因此现有存储插件不需要实现 spill 专用接口。Bub 依次写入 UTF-8 安全的 chunk,最后写入 manifest;manifest 是该结果已完整存储的提交标记。
主 tape 只保留有界预览和 opaque handle,不保存完整结果。spill.read 工具按 handle 与 cursor 有界读取,也支持从末尾开始读取。由 spill.read 明确返回的内容会作为普通的有界 tool result 记录。
sidecar 与 session tape 一起参与 fork、merge、archive 和 reset,但始终是独立 tape,构造主 context 时不会扫描它。也可以通过 Tape.archive_sidecar("spill") 和 Tape.reset_sidecar("spill") 单独 archive 或 reset sidecar。当 reset 要求先 archive 时,如果 archive 失败,Bub 会保留 sidecar。
spill 配置归 sidecar 插件自己所有:在 config.yml 使用 spill.threshold,或设置环境变量 BUB_SPILL_THRESHOLD。设为 0 只停止新写入,不会卸载 sidecar,因此已有 handle 仍可读取,tape 生命周期操作也仍会包含它。
spill 写入以 spill.write event 记录。框架管理的生命周期结果使用 sidecar.archive、sidecar.reset 和 sidecar.merge,其数据会标明受影响的 sidecar 或物理 tape。它们会在任何 context selector 运行前被排除,因此可用于运维和审计,同时不会改变模型消息或 prompt cache 前缀。sidecar 持久化失败会被记录,但不会阻止主 tape merge 或 reset。
ensure_bootstrap_anchor
Section titled “ensure_bootstrap_anchor”在某条 tape 的第一次 turn 之前,Tape.ensure_bootstrap_anchor 会检查是否存在 anchor entry。如果没有,则写入一条 session/start handoff,state={"owner": "human"}。这保证每条 tape 在 context 重建时都有起始 anchor。
default_tape_context:entry → OpenAI 消息
Section titled “default_tape_context:entry → OpenAI 消息”default_tape_context() 构造一个 TapeContext,其 select 函数(_select_messages)遍历 tape entry 并产出 OpenAI 兼容的消息。映射规则如下:
anchor→ 形如[Anchor created: <name>]: <state-as-json>的assistant消息。message→ 直接以 entry payload 作为 chat 消息 dict。tool_call→ 带空 content 与tool_calls数组的assistant消息。tool_result→ 每个 result 一条toolrole 消息;若前面有 pendingtool_call,Bub 会按 result 位置复制对应 call 的id到tool_call_id。
context selector 本身是个 hook(build_tape_context),插件可以用其他策略(压缩、摘要、检索)替换它,而无需触动 pipeline 其余部分。
带有 context=False 标记的 entry 会在 selector 运行前被移除。spill 运维 event 因此仍可在 tape 上查询,但不会进入默认 context 或插件自定义 context。
session 与 turn 隔离
Section titled “session 与 turn 隔离”每个 session id 对应一条普通 session tape。inherited subagent 重新寻址 parent session id,临时 subagent 使用一次性 session id,命名 subagent session 则寻址自己的 tape。
builtin forkmerge sidecar 通过内存 tape overlay 隔离一次 turn。调用方 merge 持久运行或 discard 临时运行。sidecar 不创建逻辑 session,也不创建持久 fork tape。
context 溢出时的 auto_handoff
Section titled “context 溢出时的 auto_handoff”内置 Agent 循环会捕捉匹配 _is_context_length_error 的模型错误(如 context length、maximum context、token limit、prompt too long)。当此类错误触发且 MAX_AUTO_HANDOFF_RETRIES(当前为 1)尚未耗尽时,循环会:
- 写入名为
auto_handoff/context_overflow的 handoff,state={"reason": "context_length_exceeded", "error": <message>}。 - 记录
loop.step事件,status="auto_handoff"。 - 用同一 prompt 重试 —— 默认
TapeContext会选择最新 anchor 之后的 entry,因此重试使用更短的重建历史。
每次 turn 仅重试一次。如果 prompt 或保留 state 仍然过大,重试仍可能失败;此时错误会正常抛出。
- Surfaces — channel 与 tool 与 tape 的关系。
- Turn pipeline — context 构造在一次 turn 中的位置。
- tape.systems — 更深入的模型与参考资料。