Files
codex-app-server/docs/harness2.md
T
host 4cbdc41e20 fix: task files endpoint resolves tenant docker worktrees
GET /tasks/{id}/files previously returned 409 for all docker tasks
on the grounds that tenant docker shares /app/work per project.
Production inspection shows the worker creates a per-task
host-worktrees/<task-id> subdirectory under the tenant work dir
(62 such dirs under the wwww tenant), so per-task reads are
attributable after all: the endpoint now resolves tenant docker
tasks via tenants/<project>/work/host-worktrees/<task-id> (local
worktree as fallback), never serves the shared /app/work mount
itself, and still 409s cloud/cross-host tasks. Docs and the
harness2_tasks boundary notes are updated to match.
2026-10-29 23:16:01 +08:00

189 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Harness² 融合说明
状态:方法层(`cloud/harness2.py` + `cloud/tests/test_harness2.py` +
`examples/harness2_demo.py`)与接线层(`cloud/harness2_tasks.py` +
`cloud/tests/test_harness2_tasks.py` + `examples/harness2_run.py`)
均已落地,在独立分支 `feat-harness2`(已 rebase 到生产 #133 的
c757810)上,不碰主线与生产,**未合并、未部署**。控制台评审页
与 `auto_apply` 开关仍是下一步,见文末。
## 上游是什么
Google Research 的 harness2(Apache 2.0,arXiv:2610.14224):模型权重
不动,只优化模型周围的 harness——系统提示、guardrails、记忆、工具
指引、子代理、技能、脚本、插件。两层递归:
- 任务层:Propose–Probe–Compose。对同一任务提 INV(通用做法)与 DOM
(领域做法)两类候选 harness,每个候选真实跑一次(探针),合成器
只把有行为证据支撑的改动合成终版;没有标准答案,靠行为对比。
- 经验层:采纳后反思出一条有证据的经验(cue / lesson / evidence),
连同被采纳 harness 的完整树存进分领域经验库,供后续任务参考。
上游报告:单步改进在三个专业 benchmark 上最高 +11.5 / +4.5 /
+11.8 个点;k=5 并行递归在 WorkBuddy 上到 80.8。这些数字是上游
benchmark 上的,不是本项目的承诺——本项目没有 benchmark,接线后
必须先在自己的重复任务集上跑 base 对照,才能谈收益。
## 融合映射:照搬 / 改写 / 不引
| 上游东西 | 本项目做法 |
|---|---|
| c1–c8 可编辑树(codex surface) | 照搬:AGENTS.md(8000 字符上限)、`.agents/skills/*/SKILL.md`、`.agents/skills/*/scripts/*`、`sub_agents/*/AGENT.md`、`scripts/*.py`、`.codex/hooks.json`;白名单外一律不读不写;校验含技能 description、触发行、脚本 compile、脚本引用存在、钩子 JSON 与事件名 |
| Propose–Probe–Compose,parallel / sequential | 照搬语义,改写实现:`run_parallel` / `run_sequential`,提议被拒给一次按反馈修正的机会,再拒回落到当前 harness;探针失败记为死探针,不炸整轮 |
| 匿名选择(只比产物) | 照搬:终版产物按任务+k+模式定种子洗牌匿名化,选择器看不到 harness 来源;选失败回落到已交付终版;k=1 且终版池只有唯一候选时 trivial 直出(与选择器是否提供无关;2026-10-29 对照纠正了此前“无选择器才 trivial”的误写) |
| 经验库(分领域、整树 + EXPERIENCE.md + INDEX.md) | 照搬存储格式,改写生效语义:入库 ≠ 自动生效(见下) |
| Ledger(会话/探针账本)与成本 | 改写:成本汇总进 `ModeResult.total_cost_usd`,由调用方接现有 `cloud/cost.py` / 租户配额计费;另有 `budget_usd` 超支门与 `max_sessions` 会话数门(计探针 + 阶段尝试会话;edit 内 fmt2 纠正与选择/反思会话不计入):任一超支后不再调用编辑器/探针,剩余阶段回落,且该轮不入库;上游的 R=4k/P=3k 事后强校验未引(本项目结构与上游计数不完全同构,门控代替断言) |
| run_tag 形状与建任务前预检 | 对照补齐:`validate_run_tag`(字符集、首字符不为 -、不以 s 数字段开头)与 `preflight_checks`(base 目录与 AGENTS.md、source 目录、提示、tag、控制面 health 探针);CLI `--check` 只预检不建任务 |
| 分角色模型(SOLVER / IMPROVER / JUDGE) | 已引(机制):`run_improvement_via_tasks` 的 `role_models`(probe / editor / selector / reflector)+ CLI `--model-<角色>`;非空值覆盖该角色,缺键回落共享 `--model`,两者都空走服务端默认——默认行为不变,模型选择权仍在调用方(有价模型产生真实成本) |
| 产物资产(deliverable_files / deliverable_assets / images)与选择 assets note | 已引(两层):①服务端 `GET /tasks/{id}/files`(任务 worktree 读取,路径安全解析、文本 ≤256KB、列表 ≤200 条;本地任务走本地 worktree,租户 Docker 任务走 `tenants/<项目>/work/host-worktrees/<任务id>`——worker 按任务建子目录,只 serves 任务级子目录、绝不 serves 共享的 `/app/work` 本身;cloud/跨机任务 409);②探针回复里的 `DELIVERABLE FILE` 块(所有执行器可用,端点结果优先、坏块丢弃不炸轮次)。产物进 `Rollout.deliverable_files`,并写入选择/编辑/反思上下文的 `deliverables/<label>/files/`。images(二进制产物)未引:端点只读 UTF-8 文本 |
| sweep 层(多任务批量、跨任务 library restore、dead 隔离、report 汇总) | 已引:`run_sweep` + CLI `examples/harness2_sweep.py`(共享 library_root、单轮失败隔离、sweep-report.json 汇总;manifest 预检不过不建任务,默认只打印计划,`--yes` 才真跑) |
| 提示词 | 改写短版(中文项目语境下的英文任务提示,结构同上游,措辞自写),不抄上游长提示 |
| Benchmark 适配器(JobBench / LAB / WorkBuddy)、评分器、rubric | 不引。任务就是一个普通 codex 任务,delivered 由 adapter 定义(如产物存在且非空) |
| Proposer 通道(Claude CLI / OpenCode)、Vertex 配置 | 不引。编辑器/选择器/反思器是调用方注入的协议,真实接线时是 codex 任务(见下) |
| 上游执行默认:网络不受限、Codex sandbox 禁用 | 明确不引,见安全一节 |
| 上游 ThreadPool 并行探针 | 不引。核心循环串行跑探针;并行是调用方在 adapter 层把每个探针发成独立 codex 任务来实现,避免核心循环抢同一个 Worker/DB |
## 安全偏差(刻意与上游不同,勿“纠回”上游)
1. **执行面与本模块解耦。** `cloud/harness2.py` 不跑任何命令、不拼
任何 codex exec 参数。真实 adapter 必须走 Worker 已有的 Docker /
gVisor 沙箱、租户隔离与 egress 白名单,绝不加
`--dangerously-bypass-approvals-and-sandbox`。上游 README 明写其
Codex 执行是 sandbox 禁用、网络不受限——那是 benchmark 实验的
默认,不是生产服务的默认。
2. **采纳 ≠ 自动铺开。** 多租户下,一个编辑器写出的 AGENTS.md 若
自动改写别人的任务,是质量与越权双重风险。所以 `improve_task`
的 `auto_apply` 默认关:库只是“待评审的经验”,提议器可读,
但任何任务的 base 永远是项目自己的 base harness;只有项目显式
开启 auto_apply 后,调用方才在下一轮的 `base_harness()` 里取库
中最新采纳树。这个开关的服务端接线(项目设置 + 控制台评审页)
是下一步。
3. **预算是硬门。** k 硬上限 3;parallel 一轮最多 10 次执行
(1 base + 2k 候选 + k 终版),sequential 同样最多 10 次。
`budget_usd` 超支后不再花钱,且该轮不入库——超支轮次的“经验”
是不完整证据,不配进库。
4. **编辑器写出的东西先校验后采纳。** 校验失败的提议给一次修正
机会,再失败就回落,绝不把坏树(空 AGENTS.md、坏钩子、不编译
的脚本、无触发行的技能)铺到探针里。
## 接口(调用方视角)
```python
from cloud.harness2 import improve_task, Library
from pathlib import Path
result = improve_task(adapter, editor,
selector=selector, reflector=reflector,
work_dir=Path("runs/h2-task-1"), k=1, mode="parallel",
library=Library(Path("runs/library"), domain="accounting"),
budget_usd=5.0)
result.to_dict() # 阶段、回落、选择、经验、入库位置、总成本
```
- `TaskAdapter`:`task_id / domain / prompt` 属性;`base_harness()`;
`execute(label, harness) -> Rollout`(失败也返回 Rollout,不抛);
可选 `validate_tree()` 做项目特有校验。
- `Editor.edit(mission, context_dir, label) -> EditResult`:读
`context_dir` 下的 `task.md / rollouts / deliverables / harness /
library`,改 `context/harness/`,返回改完的树(`tree=None` 时
框架自动从 `context/harness/` 读回——真实 codex 任务接线就用这条
路);`notes` 是编辑器最终消息里 `<notes>` 块的解析结果,由调用
方在构造 `EditResult` 时填好。
- `Selector.select(mission, context_dir) -> (id, why, cost)`:读
`context_dir/candidates/<id>.md`(匿名产物),返回 choice id。
- `Reflector.reflect(mission, context_dir) -> dict`:返回含 cue /
lesson / evidence(可含 confidence / edits)的经验,三项不全
返回 `{}` 即表示无可入库经验。
证据上下文由 `build_context` 生成:证据以文件形式给模型,prompt
只写指针,不把轨迹截断后塞进 prompt(截断过的轨迹会编造缺陷,
这是上游明确反对的做法)。
## 接线层(已落地,`cloud/harness2_tasks.py`)
只用控制面已有的 API,不新增任何服务端端点:`POST /uploads` →
`POST /tasks`(`upload_id` 绑定 + `idempotency_key` 幂等)→
`GET /tasks/{id}` 轮询终态 → `GET /tasks/{id}/trace` 读轨迹 →
`GET /tasks/{id}/cost` 读 `usd_micros` → 超时 `POST /tasks/{id}/cancel`。
认证是 `X-API-Key` 请求头;密钥只由调用方持有,接线层不读、不存、
不打印密钥。
- **探针**:bundle = 源文件 + harness 树,Worker 派发时经
`host.py --seed-dir` 拷进 worktree,harness 文件覆盖仓库同名文件
——同一任务在不同 harness 下跑。prompt = 原任务 prompt + 交付脚注。
- **结果读取**:控制面返回的 `result_text` 是一层 JSON 信封
(task / thread_id / cwd / status / text / usage),agent 的最终
回复在 `text` 字段里。`run_task` 是四路共用的 choke point,
在此用 `unwrap_result_text` 拆信封;直接拿信封当回复会让
行首锚定的文件块解析永远看不见块(2026-10-29 三轮真实轮次
的真根因:编辑明明发了块,却被判成无改动/格式错误)。
- **编辑**:bundle = `context/` 前缀的证据树;agent 在 worktree 的
`context/harness/` 里改文件,最终回复用文件块(`=== HARNESS FILE:
路径 ===` … `=== END HARNESS FILE ===`)只输出改动文件,没改的
省略即继承,外加 `<notes>` 块;接线层解析后落到 fallback 树上再
走核心校验。格式契约(2026-10-29 两轮真实轮次后收紧):只在
`<notes>` 里声称改动、却不发文件块,不再算“无需改动”——无改动
必须用显式 `<no-change>理由</no-change>` 标记;文件块与标记同
时出现、路径越界、块未闭合都算格式错误,接线层用独立幂等键
带反馈重试一次(`.fmt2`),两次都错才如实回落,绝不静默采纳。
- **选择 / 反思**:同款 bundle + 最终回复解析(` ```choice ` +
`<why>`;`<experience>` + `<edits>`),不读 worktree 文件。
- **幂等**:`idempotency_key = h2-{run_tag}-{role}-{label}`,同一
run_tag 重跑复用已有任务(断点续跑);想从头重跑必须换 run_tag,
换了 harness 内容却沿用旧 run_tag 时旧结果会被复用、白跑一轮。
- **成本与审批**:接线层不拼任何 codex 参数、不改任务默认值;
model / effort / approval / environment_id / executor 全由调用方
透传,缺省走 Worker 默认值;任务走与普通任务完全相同的执行器、
沙箱、租户隔离、配额门与审计;单任务超时默认 3600 秒,超时先
取消再记失败/回落;长任务走 #133 的自动续跑。
- **接线层最大的适用边界**:探针交付物 = 最终回复文本。文件型
产物(代码 diff、报告文件)目前只能靠 agent 在最终回复里复述/
摘要:worktree 快照 API 只给 diff 截断(20000 字)与未跟踪文件
列表、不给完整文件内容,跨主机还读不到。文件产物评审需要新增
文件读取端点,另行评审,不在本层假装已解决。
用真实任务跑一轮(先 dry-run 看计划,确认后加 `--yes`):
```bash
export CODEX_API_KEY='...' # 密钥只走环境变量,不接受命令行参数
python3 examples/harness2_run.py --base-url http://127.0.0.1:8765 \
--project-id <测试项目> --prompt-file task.md \
--base-harness-dir ./my-base-harness --source-dir ./task-source \
--mode parallel --k 1 --budget 5 --output runs/h2-real-1
# 计划无误后,加 --yes 重跑;同一 run_tag 可断点续跑
```
## 仍未做的事(需另行评审)
1. **人工关口**:采纳的 harness 先在控制台以“待评审经验”展示,
项目负责人点“采纳为 base”后才影响后续任务;`auto_apply` 开关
默认关,开启需项目负责人显式操作并记审计。目前
`run_improvement_via_tasks` 的 `auto_apply` 恒为 False,库只是
待评审经验。
2. **文件产物评审**:新增读取已终态任务 worktree 文件的端点(权限、
跨主机、大小上限都要定),或让探针任务把产物文件内容写进最终
回复的约定成为正式契约——前者是服务端改动,后者是提示词契约,
各有利弊,需评审。
3. **效果验证**:先在 3–5 个重复性任务(如固定格式的对账、周报
生成)上跑 base 对照:同一任务、同一模型,base 产物 vs 改进后
产物由人工或固定检查脚本判定,记录执行次数与成本;没有对照
就不宣称收益。
## 演示与测试(无模型、无网络)
```bash
python3 examples/harness2_demo.py --mode parallel # 应输出 Total: 85.15 并入库
python3 examples/harness2_demo.py --mode sequential
python3 -m unittest discover -s cloud/tests -p "test_harness2*.py" -v # 25 项
```
接线层单测用进程内假客户端(探针真跑 bundle 里的脚本、编辑按
文件块格式回改动)跑通完整轮次,并对本地假服务端实测了 HTTP
客户端(建任务、轮询、成本、轨迹、密钥头、multipart 与服务端
解析器往返)。真实任务链路尚未实测:第一次真实运行请用测试项目,
并先跑 `examples/harness2_run.py` 不带 `--yes` 的计划模式。
演示账本 100.25 / -20.10 / 5.00:base 脚本只加正数得 105.25,
改进后带符号和为 85.15;演示用的编辑器/选择器/反思器是确定性的,
只为证明循环本身,真实收益必须用第 4 步的对照来量。