# Mini Harness：把 Agent 的“墙面”一层层剖开

这是一个真正可运行、零第三方依赖的 Agent Harness 教学脚手架。它不调用真实 LLM，也不需要 API Key：`ScriptedModel` 会按固定脚本返回结果，因此每次运行都可复现，失败场景也能稳定进入测试。

> **证据边界：** 本 Lab 中的人物、访谈、公司规则、版本、洞察和运行结果全部是确定性的教学假数据，不对应任何真实企业、用户或研究结论。

要求：**Node.js 20 或更高版本**。

## 一条命令运行

从网站仓库根目录执行：

```bash
node public/labs/mini-harness/run.mjs all
```

如果你只下载了本目录，则进入目录后执行：

```bash
node run.mjs all
node run-research.mjs
node --test *.test.mjs
```

命令会运行四个场景，并用 Outcome Verifier 逐项验收。全部通过时进程退出码为 `0`；任何验收失败时退出码为 `1`，可直接放进 CI。

## 先看成品，再看墙体切面

一次 Agent 运行不是“问模型一次”。它是一条受控循环：

```text
任务
  ↓
检查上下文预算 → 调用模型 → 最终回答 ──────────────→ 结果验收
                         │
                         └→ 工具调用
                              ↓
                        参数校验
                              ↓
                    权限 allow / ask / deny
                              ↓
                    执行工具并记录结果
                              ↓
                    Transcript + Checkpoint
                              └──────────────→ 下一轮
```

每一层解决不同的问题：

1. **ScriptedModel（模型适配层）**：只负责提出下一步。它没有工具权限，也不能直接修改状态。
2. **Agent Loop（编排层）**：反复执行“看上下文—问模型—处理工具或结束”，并设置最大轮次。
3. **Tool Registry（行动层）**：只开放 `read_file`、`search_files`、`write_file` 三个窄工具；每个参数都做类型、长度、未知字段和路径越界校验。
4. **Permission Policy（治理层）**：只读操作 `allow`；一般写入 `ask`；密钥文件写入 `deny`。`deny` 的优先级高于一般写规则。
5. **Event Stream（可观测层）**：模型请求、参数校验、权限决策、工具成功/失败、上下文熔断都有递增序号事件。
6. **Transcript（事实账本）**：保存 user / assistant / tool / system 事实。下一轮模型看到的是这份账本，而不是 Harness 的隐式状态。
7. **Checkpoint（恢复边界）**：保存轮次、模型游标、Transcript 和工作区快照。这个示例使用内存存储；生产环境可替换成数据库或对象存储。
8. **Outcome Verifier（结果验收层）**：不以“模型说完成了”为准，而是检查状态、事件和外部结果。
9. **Eval Runner（回归层）**：隔离运行每个场景，汇总通过率，并用退出码告诉 CI 是否放行。

## 四个场景分别教什么

| 场景 ID | 故障/路径 | 应观察的系统行为 |
| --- | --- | --- |
| `normal` | 正常读取并修改配置 | 写入命中 `ask`，批准后执行，再回读验证 |
| `permission-denied` | 请求改写 `.env` | 命中 `deny`，不弹审批、不执行工具、不绕过策略 |
| `tool-failure` | 读取了错误路径 | 记录结构化失败，搜索正确路径后恢复，不重复盲撞 |
| `context-limit` | 工具结果撑爆上下文 | 在下一次模型调用前熔断，保存失败 Checkpoint |

也可以只运行一个场景：

```bash
node run.mjs tool-failure
```

## 标准扩展示例：用户研究 Agent

`research-extension.mjs` 是本课作业的完整可运行参考实现。它不修改 `MiniHarness.run` 主循环，只通过 `createToolRegistry(workspace, extensions)` 注册一个窄工具，并组合独立 Policy 与 Outcome Verifier：

```text
六份脱敏教学访谈（18 个稳定 source_id）
            ↓
 search_interviews：query / limit / includeSensitive
            ↓
 Policy：普通访谈 allow；受限原文 ask；未知能力 deny
            ↓
 ScriptedModel 提出检索并生成三条候选洞察
            ↓
 Verifier 重新读取 fixture，逐项核对来源 ID、逐字原句、文件覆盖与敏感授权
```

运行标准答案：

```bash
node run-research.mjs
```

预期结果是三条洞察通过四项独立验收。模型即使写出“已完成”，只要存在以下任一情况，Verifier 都会拒绝：

- 不是恰好三条洞察；
- 每条少于两个唯一 `source_id`；
- `source_id` 不存在，或 `exact_quote` 不是 fixture 中的逐字原句；
- 总计没有覆盖至少三份访谈；
- 引用了受限来源，却没有对应敏感检索的批准事件。

### 已备齐的练习材料

- `fixtures/interviews/`：6 份脱敏教学假访谈，每份 3 个稳定来源，共 `I01-S01` 至 `I06-S03` 18 个来源。
- `fixtures/restricted/raw-participant.md`：1 份故意包含虚构姓名与邮箱的受限材料，用于验证 `ask` 与零泄露拒绝路径。
- `research-extension.test.mjs`：验证 fixture 数量、Schema、普通/敏感 Policy、拒绝零执行、批准读取和 Verifier 正反例。

`search_interviews` 的输入合同：

```json
{
  "query": "来源",
  "limit": 8,
  "includeSensitive": false
}
```

- `query`：2–80 个字符；
- `limit`：1–20 的整数，默认 8；
- `includeSensitive`：布尔值，默认 `false`；设为 `true` 必须经过 `ask`；
- 任何未知字段都会在 Policy 之前返回 `INVALID_ARGUMENT`，因此调用者不能自己传路径绕过目录边界。

标准输出合同：

```json
{
  "claim": "一条可检查的洞察",
  "source_ids": ["I01-S02", "I02-S02"],
  "exact_quotes": {
    "I01-S02": "fixture 中的逐字原句",
    "I02-S02": "fixture 中的逐字原句"
  }
}
```

## 按这个顺序读代码

1. 打开 `scenarios.mjs` 的 `normal`，先看模型脚本、初始工作区和验收条件。
2. 在 `core.mjs` 搜索 `MiniHarness.run`，沿着一个 turn 阅读主循环。
3. 搜索 `createToolRegistry`，观察参数校验与执行函数为何分开。
4. 搜索 `createDefaultPolicy`，比较三条规则的顺序。
5. 搜索 `#saveCheckpoint` 和 `#fail`，看成功路径与失败路径如何留下证据。
6. 回到 `runEvalSuite`，看场景如何隔离以及 Verifier 如何决定通过与否。

## 自己动手的练习

1. 把 `normal` 的 `answerPermission` 改成 `false`。预测哪些事件仍会出现、工作区是否变化，然后再运行。
2. 给 `write_file` 多传一个 `reason` 字段。观察参数校验为何在权限判断之前失败。
3. 新增 `delete_file` 时，先写 `deny` 默认策略与 Eval，再实现工具。不要让“模型会谨慎”成为安全边界。
4. 阅读 `research-extension.mjs`，把 `search_interviews` 换成另一个窄工具；保持 Schema、Policy、Verifier 和正反测试同时更新。

## 从教学脚手架换成生产系统

这个 Lab 刻意把外部依赖留在边界上。生产化时按接口替换，而不是把主循环推倒重写：

- 用真实 LLM Adapter 替换 `ScriptedModel`，同时保留确定性模型用于 Eval。
- 用目标模型的 tokenizer 替换 `ContextBudget` 中的字符估算，并增加压缩/摘要策略。
- 把 `MemoryWorkspace` 换成受沙箱约束的文件、浏览器或业务 API；工具仍保持窄参数与结构化错误。
- 把 `answerPermission` 接到真实用户界面；审批决定必须与 tool call id 绑定，并设置超时。
- 把 Event、Transcript、Checkpoint 写入持久化存储；敏感参数应脱敏，日志应有保留周期。
- 把 Verifier 扩展为状态断言、规则检查、模型评分和人工抽检的组合，避免单一“LLM 评 LLM”。

最重要的边界没有变：**模型提出行动，Harness 决定行动能否发生；模型报告完成，Verifier 决定目标是否真的完成。**

## 文件地图

```text
mini-harness/
├── fixtures/
│   ├── interviews/             # 6 份教学假访谈，18 个稳定 source_id
│   └── restricted/             # 受限假材料，验证敏感审批
├── core.mjs                    # Loop、模型、工具、权限、事件、记录、检查点、验收器
├── scenarios.mjs               # 四个确定性场景与 Eval runner
├── research-extension.mjs      # 窄工具 + Policy + Verifier 标准扩展示例
├── run.mjs                     # 四终态命令入口与 CI 退出码
├── run-research.mjs            # 用户研究标准答案入口
├── mini-harness.test.mjs       # 核心四终态与边界测试
├── research-extension.test.mjs # 扩展 Schema、权限与验收正反测试
├── package.json                # Node 20+ 声明，无 dependencies
└── README.md                   # 本教程
```
