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 名(TapeService.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_sidecars 挂载 SpillStore、注册 spill.read,并通过现有 after_tool_call hook 限制大结果。sidecar 本身没有工具结果拦截约定。较大的结果存放在名为 <session-tape>__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 之前,TapeService.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。
fork_tape
Section titled “fork_tape”TapeService.fork_tape(tape_name, merge_back=True) 是由 ForkTapeStore.fork 支撑的 async context manager。块内的写入发生在 fork 后的 tape 上;退出时合并回父 tape(若 merge_back=False 则丢弃)。可以用它跑子任务,避免污染父 session 的历史,直到决定保留结果。
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 — 更深入的模型与参考资料。