86 lines
4.9 KiB
Markdown
86 lines
4.9 KiB
Markdown
# 落盘存储
|
||
|
||
[English](spill.md) | 中文
|
||
|
||
落盘存储 seam 是一项[能力 seam](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md),它持久保存工具的超大文本,并返回面向模型的定位符与检索指引;该能力拆分到三个包:接口([dsh-spill](../../packages/spill/spill),`ctx.spillStore`)、实现([dsh-spill-local](../../packages/spill/spill-local),宿主文件系统中会话作用域的私有文件)和消费方([dsh-spill-policy](../../packages/spill/spill-policy),`tools/post-execute` 策略)。落盘是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇记录在此处,而不在 [core.md](core.md) 中。预览机制仍归 [dsh-retention](../../packages/util/retention) 所有;该 seam 只保存策略交给它的最终文本。
|
||
|
||
源码:[`packages/spill/spill/src/types.ts`](../../packages/spill/spill/src/types.ts)
|
||
|
||
## 保存请求
|
||
|
||
`saveText` 是整个 seam:原样持久保存 `content`,并返回不透明的定位符、后端提供的检索提示和准确字节数。请求携带保存时的存储命名空间(`owner`)、内容来源(`source`,用于命名和检查的描述性来源信息,而非访问控制)以及后端可用作命名提示的 `suggestedName`(它不是路径)。
|
||
|
||
```ts type-equiv
|
||
/** One request to persist text to a spill artifact. */
|
||
interface SaveTextSpill {
|
||
owner: SpillOwner
|
||
source: SpillSource
|
||
/**
|
||
* A caller-suggested base name (e.g. `web_fetch.txt`). The backend sanitizes
|
||
* it to a single safe path segment before use — it is a hint, never a path.
|
||
*/
|
||
suggestedName: string
|
||
/** The full text to persist (UTF-8). */
|
||
content: string
|
||
}
|
||
```
|
||
|
||
```ts type-equiv
|
||
/**
|
||
* Save-time storage namespace for a spilled artifact. The session id lets a
|
||
* backend group storage under the producing session, but the returned
|
||
* {@link SpillLocator} is the model-facing handle. Forked sessions inherit
|
||
* locators already present in the seeded log; those artifacts are not copied or
|
||
* re-owned, and spills produced after the fork use the child session id.
|
||
*/
|
||
interface SpillOwner {
|
||
sessionId: SessionId
|
||
}
|
||
```
|
||
|
||
`SpillOwner.sessionId` 是保存时的存储命名空间。fork 后的会话会从种子日志继承已有的落盘定位符;这些产物不会被复制或重新取得所有权,fork 后产生的落盘则使用子会话 id。保留期清理可以连同其他旧会话产物一起使旧定位符失效;落盘 seam 不定义逐会话的清理策略。
|
||
|
||
```ts type-equiv
|
||
/**
|
||
* Provenance of one spilled artifact — recorded by the backend for a readable
|
||
* filename and inspection. Not interpreted for access control; purely
|
||
* descriptive.
|
||
*/
|
||
interface SpillSource {
|
||
/** The tool whose result was spilled (e.g. `web_fetch`). */
|
||
toolName: string
|
||
/** The model-issued call id the result belongs to. */
|
||
callId: CallId
|
||
/** A short human label for the artifact (e.g. `result`). */
|
||
label: string
|
||
}
|
||
```
|
||
|
||
## 结果
|
||
|
||
```ts type-equiv
|
||
/** A saved spill artifact: its locator, byte length, and backend-specific retrieval guidance. */
|
||
interface SpillRef {
|
||
locator: SpillLocator
|
||
bytes: number
|
||
retrievalHint: string
|
||
}
|
||
```
|
||
|
||
`SpillLocator` 是后端返回的[品牌化](core.md#branded-ids)面向模型句柄。本地后端将它渲染为文件系统路径;远程或数据库后端可以渲染 URI、键或命令 token。消费方将它视为不透明值,并使用 `retrievalHint` 渲染,而不是假定 `read` 始终是正确的检索机制。
|
||
|
||
```ts type-equiv
|
||
/**
|
||
* Opaque model-facing handle for one spilled artifact. A local backend may use a
|
||
* filesystem path; a remote or database backend may use a URI or key. Consumers
|
||
* render it with {@link SpillRef.retrievalHint}, but do not parse it.
|
||
*/
|
||
type SpillLocator = Branded<'SpillLocator'>
|
||
```
|
||
|
||
## 服务
|
||
|
||
`SpillStore`(`ctx.spillStore`,定义于 [`packages/spill/spill/src/index.ts`](../../packages/spill/spill/src/index.ts))是只有一个方法的抽象服务:`saveText(input) → Promise<SpillRef>`。它持久保存完整的 `content`,并在实际存储失败(权限、ENOSPC、后端不可用)时拒绝。该 seam 只负责存储:不负责保留策略、工具结果替换或检索/搜索 API。
|
||
|
||
本地后端([dsh-spill-local](../../packages/spill/spill-local))写入 `<root>/session-<hash>/<random>-<safeName>`:根目录是已配置或延迟创建的私有(0700)目录,会话子目录采用 `sha256(sessionId)`,并通过排他的仅所有者可访问写入(`open(path, 'wx', 0o600)`)防止预先植入的符号链接重定向写入。其 `locator` 是本地路径,`retrievalHint` 则告知模型在该路径上使用 `read` 或 `grep`。策略消费方([dsh-spill-policy](../../packages/spill/spill-policy))会把超过 `maxInlineBytes` 的纯文本最终结果替换为保留库生成的首尾预览和落盘引用;该过程尽力而为:保存失败时保留原始内联结果,而不会把成功的调用变成 `isError`。
|