- select_final trivial condition now k==1 and single-candidate pool, regardless of whether a selector is provided (previously gated on selector is None, so wired runs always paid for a useless selector task at k=1) - Budget gains max_sessions gate (probe + stage-attempt sessions; fmt2 corrections and selector/reflector sessions exempt), the backstop for unpriced models where the USD gate never binds - validate_run_tag (charset, no leading '-', no s<N> seed prefix) enforced at run_improvement_via_tasks and the CLI - CLI --check preflight (base harness, source dir, prompt, tag, control-plane /health probe) without creating tasks - ModeResult.to_dict now includes rollout summaries so a lost probe is visible in full_result instead of only on the server - CLI plan counts the selector only when k>1
13 KiB
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) | 未引:全部角色共用调用方透传的同一个 model;需用户定各角色模型才有意义 |
| 产物资产(deliverable_files / deliverable_assets / images)与选择 assets note | 未引:探针交付物目前只有最终回复文本;需新增 worktree 文件读取端点后与该端点一起做 |
| sweep 层(多任务批量、跨任务 library restore、dead 隔离、report 汇总) | 未引:单任务产品形态用不上,批量改进需求出现再做 |
| 提示词 | 改写短版(中文项目语境下的英文任务提示,结构同上游,措辞自写),不抄上游长提示 |
| Benchmark 适配器(JobBench / LAB / WorkBuddy)、评分器、rubric | 不引。任务就是一个普通 codex 任务,delivered 由 adapter 定义(如产物存在且非空) |
| Proposer 通道(Claude CLI / OpenCode)、Vertex 配置 | 不引。编辑器/选择器/反思器是调用方注入的协议,真实接线时是 codex 任务(见下) |
| 上游执行默认:网络不受限、Codex sandbox 禁用 | 明确不引,见安全一节 |
| 上游 ThreadPool 并行探针 | 不引。核心循环串行跑探针;并行是调用方在 adapter 层把每个探针发成独立 codex 任务来实现,避免核心循环抢同一个 Worker/DB |
安全偏差(刻意与上游不同,勿“纠回”上游)
- 执行面与本模块解耦。
cloud/harness2.py不跑任何命令、不拼 任何 codex exec 参数。真实 adapter 必须走 Worker 已有的 Docker / gVisor 沙箱、租户隔离与 egress 白名单,绝不加--dangerously-bypass-approvals-and-sandbox。上游 README 明写其 Codex 执行是 sandbox 禁用、网络不受限——那是 benchmark 实验的 默认,不是生产服务的默认。 - 采纳 ≠ 自动铺开。 多租户下,一个编辑器写出的 AGENTS.md 若
自动改写别人的任务,是质量与越权双重风险。所以
improve_task的auto_apply默认关:库只是“待评审的经验”,提议器可读, 但任何任务的 base 永远是项目自己的 base harness;只有项目显式 开启 auto_apply 后,调用方才在下一轮的base_harness()里取库 中最新采纳树。这个开关的服务端接线(项目设置 + 控制台评审页) 是下一步。 - 预算是硬门。 k 硬上限 3;parallel 一轮最多 10 次执行
(1 base + 2k 候选 + k 终版),sequential 同样最多 10 次。
budget_usd超支后不再花钱,且该轮不入库——超支轮次的“经验” 是不完整证据,不配进库。 - 编辑器写出的东西先校验后采纳。 校验失败的提议给一次修正 机会,再失败就回落,绝不把坏树(空 AGENTS.md、坏钩子、不编译 的脚本、无触发行的技能)铺到探针里。
接口(调用方视角)
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):
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 可断点续跑
仍未做的事(需另行评审)
- 人工关口:采纳的 harness 先在控制台以“待评审经验”展示,
项目负责人点“采纳为 base”后才影响后续任务;
auto_apply开关 默认关,开启需项目负责人显式操作并记审计。目前run_improvement_via_tasks的auto_apply恒为 False,库只是 待评审经验。 - 文件产物评审:新增读取已终态任务 worktree 文件的端点(权限、 跨主机、大小上限都要定),或让探针任务把产物文件内容写进最终 回复的约定成为正式契约——前者是服务端改动,后者是提示词契约, 各有利弊,需评审。
- 效果验证:先在 3–5 个重复性任务(如固定格式的对账、周报 生成)上跑 base 对照:同一任务、同一模型,base 产物 vs 改进后 产物由人工或固定检查脚本判定,记录执行次数与成本;没有对照 就不宣称收益。
演示与测试(无模型、无网络)
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 步的对照来量。