# dsh-tool-result-guard

[English](README.md) | 中文

**[DeepSeek Harness (DSH)](https://www.npmjs.com/package/@deepseek-ai/dsh) 的零丢失工具结果剪枝插件。** 一个 `dsh-plugin`。

超大的纯文本工具结果会**先完整落盘（spill）**，然后才被替换为有界的首尾预览；位于第 0 位的标记携带精确的被省略区间与 spill 文件定位符。模型永远可以找回中间部分；任何文本都不会在没有持久副本的情况下被丢弃。

```
[pruned: kept chars [0, 4096) + [28976, 30000) of 30000; the elided middle
[4096, 28976) is NOT lost — full output saved at: /tmp/dsh-spill/.../bash.txt.
Recover any elided span with the read tool (offset/limit), grep, or sed on that file.]

<head：前 4096 字符>

[... middle elided ...]

<tail：最后 1024 字符>
```

## 为什么

DSH 内置两套机制：

| 机制 | 触发时机 | 行为 |
|---|---|---|
| `dsh-spill-policy` | 结果 > `maxInlineBytes`（默认 50000 **字节**），post-execute 时 | 全文写入 `ctx.spillStore`，返回有界预览 + 定位符 —— **零丢失** |
| `dsh-compaction-tool-result-pruner` | 文本 > 8192 字符，**且仅在 compaction 压力满足时** | 保留头 4096 + `[... tool result middle pruned ...]` + 尾 1024。原始事件虽留在 append-only 会话日志里供 replay，但模型**没有定位符、没有偏移、也没有读日志的工具** —— 中间部分对模型不可恢复 |

缺口：约 8K 字符到 50K 字节之间、存活到 compaction 的结果会被**无恢复路径地**剪掉。本插件把剪枝提前到 `tools/post-execute`，先 spill 全文，再在标记里写入精确的省略区间 `[head, total-tail)` 和定位符。

安装后，内置 pruner 在 surface 上找不到超过 8K 字符的结果，自然空转；其行可留可删，`spill-policy` 同理（上限永远不会触达）。两者保留无害。

## 安装

一条命令：

```sh
dsh plugin --profile web add dsh-tool-result-guard
```

重启 DSH 即生效 —— 对该 profile 下的**所有**预设生效。卸载：`dsh plugin --profile web remove dsh-tool-result-guard`。

调预算：在 profile 自己的 `~/.dsh/profiles/web/cordis.patch.yml` 里按 id 覆盖：

```yaml
- id: tool-result-guard
  config:
    thresholdChars: 16384
```

<details>
<summary><b>备选：免 pnpm 的单 preset 安装</b>（只对某一个 agent preset 生效）</summary>

```sh
npx dsh-tool-result-guard install --preset my-preset --from standard
```

会把 shipped `standard` preset 复制到 `~/.dsh/.agent-presets/my-preset/`，把 `dsh-tool-result-guard.js` 放到它的 `agent.cordis.yml` 旁边并追加插件行；用 `my-preset` 开**新**会话即可。已有 user preset 用 `--preset <id>`（不带 `--from`）原地打补丁，`remove --preset <id>` 卸载，`--print` 只打印片段。等价手动安装 —— 把 `index.js` 复制到 preset 的 `agent.cordis.yml` 旁边并追加：

```yaml
- id: tool-result-guard
  name: './dsh-tool-result-guard.js'
```

</details>

## 配置

未知键在加载时报错。所有预算以 Unicode code point 计（不会拆散代理对）。

| 键 | 默认 | 含义 |
|---|---|---|
| `thresholdChars` | `8192` | 扁平化后的纯文本结果超过该 code point 数时剪枝。 |
| `headChars` | `4096` | 内联保留的头部 code point 数。 |
| `tailChars` | `1024` | 内联保留的尾部 code point 数。 |
| `excludeTools` | `["read"]` | 结果永远放行的工具。默认排除 `read` 以防止 `读 spill 文件 → 再被剪 → 再读` 循环。可改为如 `["read", "subagent", "memory_search"]`。 |
| `spillDir` | *（不设）* | 覆盖本地兜底 spill 目录（默认：OS 临时目录下的私有目录）。 |

`headChars + tailChars` 必须小于 `thresholdChars`，保证标记永远放得下。

## 行为准则

- **失败放行（fail-open）。** 没有会话属主、spill 后端不可用、写盘失败、或替换结果不满足更小/在预算内 —— 一律保留原始内联结果。剪枝绝不隐藏输出，绝不把成功调用变成错误。
- **先 spill 后剪枝。** 中间内容离开模型视野之前，全文已经持久化（优先走部署的 `ctx.spillStore`，否则写入进程私有的本地目录）。
- **放行规则。** 非 accept 决策、value 替换、嵌套复合子调用、被排除工具、含非文本块的结果（图片永不被修改）、以及阈值内的结果，原样返回。
- **可组合。** 以 prepend 方式注册 `tools/post-execute` waterfall 监听器并通过 `next()` 委托，工具自身的投影和其他 hook 先执行；被剪的是它们替换后的内容。`additionalContexts` 原样保留。
- **幂等。** 替换结果永远在 `thresholdChars` 以内，第二次经过不会再次剪枝。

## 模型如何找回中间部分

标记给出 spill 文件路径和精确的字符区间。模型用 `read`（offset/limit）、`grep -n` 或 `sed -n 'X,Yp'` 读取该文件即可 —— `read` 的默认排除保证恢复性读取自身不会被剪。

## 开发

```sh
npm test          # node:test，零依赖
```

## 许可证

MIT
