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

14 KiB
Raw Blame History

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、坏钩子、不编译 的脚本、无触发行的技能)铺到探针里。

接口(调用方视角)

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 可断点续跑

仍未做的事(需另行评审)

  1. 人工关口:采纳的 harness 先在控制台以“待评审经验”展示, 项目负责人点“采纳为 base”后才影响后续任务;auto_apply 开关 默认关,开启需项目负责人显式操作并记审计。目前 run_improvement_via_tasks 的 auto_apply 恒为 False,库只是 待评审经验。
  2. 文件产物评审:新增读取已终态任务 worktree 文件的端点(权限、 跨主机、大小上限都要定),或让探针任务把产物文件内容写进最终 回复的约定成为正式契约——前者是服务端改动,后者是提示词契约, 各有利弊,需评审。
  3. 效果验证:先在 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 步的对照来量。