Spill 是一条管理上下文溢出的能力接缝:当工具产出的内容超出放到模型上下文中的健康上限时,spill 机制把完整文本保存到持久的、会话作用域的存储,转而交给模型一个定位符(locator)加上取回指引(retrieval guidance)。大项目文件(或日志)从不进入上下文窗口,但仍然可寻址——模型稍后可以 read/grep。这是一项可选能力,并非 agent-loop 主链的一部分。
| 包 | 角色 | ctx key |
|---|---|---|
packages/spill/spill | 服务定义:SpillStore + 请求/结果类型 | ctx.spillStore |
packages/spill/spill-local | 服务提供方:宿主文件系统 LocalSpillStore | 注册为 ctx.spillStore |
packages/spill/spill-policy | 消费方:决定何时 spill 的 tools/post-execute 策略 | 不注册服务 |
架构采用 dsh 众多能力通用的接缝 + 提供方 + 策略拆分。服务定义只管存储——没有保留策略、工具结果替换或取回/搜索 API。消费方策略负责决策与通知排版;提供方负责机制。
保存请求与结果
服务只有一个操作 saveText。请求携带保存时的存储命名空间、产出工具身份、命名提示与全文:
interface SaveTextSpill {
owner: SpillOwner // { sessionId }
source: SpillSource // { toolName, callId, label }
suggestedName: string // 例如 "web_fetch.txt"——提示,绝非路径
content: string // 要持久化的完整文本
}owner.sessionId 是保存时的存储命名空间——后端子按产出会话对存储分组,但返回的定位符才是模型可见的句柄。fork 后的会话继承已存在于 seed 日志中的定位符;这些 artifact 不会被复制或重新归属,fork 之后产生的 spill 使用子会话 id。source 纯粹是描述性的(用于可读文件名与检查,绝不用于访问控制)。
interface SpillRef {
locator: SpillLocator // Branded<'SpillLocator'>——不透明、面向模型的句柄
bytes: number // 实际持久化的字节数
retrievalHint: string // 后端提供的指引(如 "Use read or grep")
}SpillLocator 故意不透明:本地后端渲染文件系统路径,但远程或数据库后端可以渲染 URI、键或命令 token。消费方用 retrievalHint 渲染它,而非解析它。
服务:SpillStore
SpillStore(ctx.spillStore,见 packages/spill/spill/src/types.ts 与 index.ts)是一条单方法抽象服务:
abstract saveText(input: SaveTextSpill): Promise<SpillRef>它持久化完整 content 原文,并在真实存储失败(权限、ENOSPC、后端不可用)时拒绝——绝不截断,也绝不把失败伪装成成功。降级由调用方决定(策略把拒绝视为尽力而为)。存储以 input.owner.sessionId 为作用域;后端必须选择私有(不公开可读)的位置,并采用派生于、绝不等于调用方 suggestedName 的无冲突名称。
本地提供方:LocalSpillStore
dsh-spill-local 以一个重视隐私的布局写入宿主文件系统:
<root>/session-<sha256-prefix(sessionId)>/<random>-<safeName>root:配置的root,否则为 OS 临时目录下惰性创建的私有(0700)进程目录(本地部署的安全默认值)。session-<hash>:以sha256(sessionId)前 12 个十六进制字符(48 位)命名的会话子目录,把存储文件绑定到其拥有会话。<random>-<safeName>:不可预知的前缀加上由suggestedName派生的、净化过的单一安全段名称。- 写入是排他且仅拥有者可写的——
open(path, 'wx', 0o600)——因此预埋的符号链接无法重定向它。spilled 的工具结果不得被其他本地用户读取。
其 locator 是本地绝对路径,retrievalHint 为 'Use read with offset/limit, or grep this path to search within it.' 因此取回契约就是在共置的本地路径上直接 read/grep。
策略:何时 spill、如何 spill
dsh-spill-policy 不是服务——它是一条 tools/post-execute 结果转换器。作为 prepended 监听器注册,它决定何时 spill 并排版替换通知。唯一配置 maxInlineBytes 是上限:
- 省略 ⇒ 插件什么都不注册(真正 no-op)。
- 设置 ⇒ 任何大于此的纯文本最终结果都会完整保存到
ctx.spillStore,并用有界的头/尾预览加上 spill 引用替换。
预览与通知来自 @deepseek-ai/dsh-output-retention 的 TextRetainer(kind: 'headTail',预算在两段间分配),附省略描述:
(Omitted N bytes. Full formatted result stored at: <locator>. <retrievalHint>)策略刻意收窄:
- 仅纯文本结果——任何非文本块会保持结果原样(策略只认识最终格式化文本)。
- 设计上尽力而为——没有会话拥有者、没有
ctx.spillStore后端或保存失败时,记日志并返回原内联结果。一次 spill 失败绝不把成功的工具调用变成isError或隐藏内联结果。 - 模型面向的支路跳过
read,以避免read → spill → read again循环(持久日志支路确实会对read子调用加限,因为日志副本不是模型上下文)。 - 第二条支路把同一上限应用到持久日志:
tools/code-dispatch-log瀑布会限定tool/code-dispatch事件里过大的run_code子调用结果副本。程序的值不受影响;UI 与重放经由 spill artifact 读取完整文本。
由此,超出上限的 read/grep 结果(来自大文件、网络抓取或搜索输出)是典型候选 spill 对象——正好对应发现工具中被保留的 sampleOverCapGlobResults 输出所标记的情形。
生命周期草图(ASCII)
tool result (oversized) → tools/post-execute (spill-policy)
│
├─ size ≤ maxInlineBytes? → keep inline (no-op)
└─ size > maxInlineBytes? →
SpillStore.saveText(full text → <root>/session-<hash>/<rand>-<safe>)
replace model result with:
[head|…|tail] (… Omitted N bytes. Full formatted result stored at: <locator>. <hint>)包
| 包 |
|---|
@deepseek-ai/dsh-spill |
@deepseek-ai/dsh-spill-local |
@deepseek-ai/dsh-spill-policy |
延伸阅读
- 上下文来源与压缩(Compaction)——保持上下文有界的另一套机制
- 工具注册表与执行流水线——
tools/post-execute扩展点 - 文件系统能力——
read/grep作为 spilled 定位符的取回路径 docs/subsystems/spill.md——官方 Spill Storage 参考packages/spill/spill/src/types.ts——SaveTextSpill、SpillRef、SpillLocator类型packages/spill/spill-policy/src/index.ts——决策逻辑与通知排版