**大结果存盘机制实现方案**

目标：当 agent 的工具输出过大时，不把完整结果塞进模型上下文，而是把完整结果写入磁盘，只把“文件路径 + 小预览 + 读取提示”放回上下文。这个机制应独立于上下文压缩。

**核心流程**

1. 工具执行完成，得到 `tool_result`
2. 判断结果是否为空、是否含图片/二进制、是否超过阈值
3. 如果没超过阈值，原样放入上下文
4. 如果超过阈值，写入磁盘
5. 上下文中替换为一段引用消息
6. 后续模型如需完整内容，再通过读文件工具读取

推荐上下文替换格式：

```text
<persisted-output>
Output too large (1.2 MB). Full output saved to: /path/to/session/tool-results/tool_123.txt

Preview (first 2000 chars):
...
</persisted-output>
```

**目录设计**

按 session 隔离：

```text
.agent/
  sessions/
    {session_id}/
      transcript.jsonl
      tool-results/
        {tool_use_id}.txt
        {tool_use_id}.json
        {tool_use_id}.bin
```

建议文件名使用 `tool_use_id`，不要用工具名或用户输入拼路径，避免路径注入和重复写入问题。

**阈值策略**

建议2层阈值：

```ts
const DEFAULT_MAX_RESULT_CHARS = 50_000
const PREVIEW_CHARS = 2_000
```

单个工具结果：

```ts
effectiveThreshold = Math.min(
  tool.maxResultSizeChars ?? DEFAULT_MAX_RESULT_CHARS,
  DEFAULT_MAX_RESULT_CHARS
)
```

特殊工具可以 opt-out：

```ts
maxResultSizeChars: Infinity
```

例如文件读取工具如果本身支持 `offset/limit`，可以不走大结果存盘，避免“读文件结果又让模型去读文件”的循环。

**核心伪代码**

```ts
async function processToolResult(tool, result, toolUseId, session) {
  const block = tool.mapResultToBlock(result, toolUseId)

  if (isEmpty(block.content)) {
    return {
      ...block,
      content: `(${tool.name} completed with no output)`,
    }
  }

  if (containsImage(block.content)) {
    return block
  }

  const size = estimateContentSize(block.content)
  const threshold = getThreshold(tool)

  if (size <= threshold) {
    return block
  }

  const persisted = await persistToolResult({
    sessionId: session.id,
    toolUseId,
    content: block.content,
  })

  if (!persisted.ok) {
    return block // 或退化为截断，看你的产品取舍
  }

  return {
    ...block,
    content: buildPersistedMessage(persisted),
  }
}
```

**落盘函数**

```ts
async function persistToolResult({ sessionId, toolUseId, content }) {
  const dir = path.join(projectStoreDir, sessionId, "tool-results")
  await fs.mkdir(dir, { recursive: true })

  const isJson = typeof content !== "string"
  const ext = isJson ? "json" : "txt"
  const filepath = path.join(dir, `${toolUseId}.${ext}`)

  const text = isJson
    ? JSON.stringify(content, null, 2)
    : content

  // 用 wx：文件已存在则不覆盖，保证重放时稳定
  try {
    await fs.writeFile(filepath, text, { encoding: "utf8", flag: "wx" })
  } catch (err) {
    if (err.code !== "EEXIST") {
      return { ok: false, error: err.message }
    }
  }

  return {
    ok: true,
    filepath,
    originalSize: text.length,
    preview: makePreview(text, 2000),
    hasMore: text.length > 2000,
  }
}
```

**预览策略**

预览不要简单硬切，最好在换行边界切：

```ts
function makePreview(text, maxChars) {
  if (text.length <= maxChars) return text

  const slice = text.slice(0, maxChars)
  const lastNewline = slice.lastIndexOf("\n")

  if (lastNewline > maxChars * 0.5) {
    return slice.slice(0, lastNewline)
  }

  return slice
}
```



**恢复与重放**

如果 agent 支持 resume，需要把替换决策写入 transcript：

```json
{
  "type": "content_replacement",
  "kind": "tool_result",
  "tool_use_id": "toolu_123",
  "replacement": "<persisted-output>...</persisted-output>"
}
```

恢复会话时，不要重新生成 preview。直接复用 transcript 里的 `replacement`，这样 prompt 前缀稳定，有利于缓存，也避免代码改动导致历史上下文变化。

**二进制与图片**

文本/JSON：走普通 `persistToolResult`

图片：通常不要把 image block 序列化存成 JSON 再给模型，应保持原始图片块或走图片压缩逻辑。

二进制文件：单独保存原始 bytes：

```text
tool-results/{id}.pdf
tool-results/{id}.xlsx
tool-results/{id}.zip
```

上下文中只返回：

```text
Binary content (application/pdf, 3.1 MB) saved to /path/to/file.pdf
```

