Skip to content

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。请求携带保存时的存储命名空间、产出工具身份、命名提示与全文:

ts
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 纯粹是描述性的(用于可读文件名与检查,绝不用于访问控制)。

ts
interface SpillRef {
  locator: SpillLocator   // Branded<'SpillLocator'>——不透明、面向模型的句柄
  bytes: number           // 实际持久化的字节数
  retrievalHint: string   // 后端提供的指引(如 "Use read or grep")
}

SpillLocator 故意不透明:本地后端渲染文件系统路径,但远程或数据库后端可以渲染 URI、键或命令 token。消费方用 retrievalHint 渲染它,而非解析它。

服务:SpillStore

SpillStorectx.spillStore,见 packages/spill/spill/src/types.tsindex.ts)是一条单方法抽象服务

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-retentionTextRetainerkind: 'headTail',预算在两段间分配),附省略描述:

text
(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

延伸阅读