执行摘要
- 一句话:共享服务器腿改原生打分,退役适配补偿
- 推荐动作:值得精读,尤其适合想理解「删除兼容分支时如何保正确性」的工程师。三个看点:(1) 运行时契约守卫的设计——当 preflight 不可行时,用回复自身的契约指纹(info.harness)把静默误打分变成大声失败;(2) 跨仓库边界的演进编排——miles 侧 PR 明确依赖上游 OpenEnv#1012 且给出双向不兼容的切换顺序;(3) review 中 Shi-Dong 抓出的 0.0 vs None 差异,是「删除看似等价代码」时最容易漏掉的行为边界。
功能与动机
PR body 说明这是 #1675 的 follow-up,解决该 PR review 中关于退役 native_evaluate=False 分支的讨论。核心动机有三:(1) 让两条 agent-function leg(共享服务器 / Daytona 沙箱)统一为单一打分协议,消除 adapter 侧针对旧 tbench2_env 部署的补偿机制;(2) 前提是上游先把 docker 模式补齐——OpenEnv#965/#972 只修了 local 模式,docker 模式仍用裸 pytest 从 /task 打分,直接翻转会造成 fidelity regression,因此先等 OpenEnv#1012 合入(04d259ea6);(3) 面对无法预检源码的远程共享服务器,用运行时契约守卫把「旧部署静默返回看似合理的错误奖励」变成「每个样本大声丢弃」,避免污染训练信号。
实现拆解
-
退役 adapter 侧补偿机制(openenv_agent_function.py,+75/-162):删除 _TASK_WORKDIR / _TB2_TESTS_SRC 两个环境变量、_apply_workdir、_CANONICAL_EVAL_CMD、_REWARD_MARKER / _TESTSH_RC_MARKER 及 _parse_reward_marker / _parse_testsh_rc 解析器,以及 testsh_rc 指标管线;模块 docstring 中的 OPENENV_TASK_WORKDIR / OPENENV_TB2_TESTS_SRC 条目随之移除。原因:这些补偿只服务于旧 tbench2_env 部署(裸 pytest 打分、WORKDIR 不在 /app),OpenEnv#1012 后服务端自身具备同款契约,补偿成为必须清理的双协议债务。
-
统一两条 leg 的 episode 接线:_multi_turn 的关键字参数从三个(run_body / native_evaluate / post_episode)减为两个(run_body / post_episode),native_evaluate 参数整体删除;openenv_daytona_agent_function.py 的 run_episode 不再传 native_evaluate=True。两条腿现在只剩两个真实差异:run_body(环境如何创建:连共享服务器 vs 建 Daytona 沙箱)与 post_episode(共享服务器需要 trial-dir 清理,一次性的沙箱不需要)。
-
新增运行时契约守卫:新增 _obs_info 辅助函数读取 observation.info;_multi_turn 的 evaluate 处理要求回复携带 info.harness == "tests/test.sh"(当前上游才会发射的标记),否则记警告并以 reward=None 丢弃样本,与 #1675 已有的 error/None-reward 丢弃逻辑叠加。这是对「远程服务器无法做源码 preflight」的替代方案;已知行为变化:不带 tests/test.sh 的任务目录(走服务端 pytest fallback,无 harness 标记)也会被丢弃——这是刻意取舍,因为该路径与旧服务器不可区分,且 89 个官方任务与合成池都带 test.sh。
-
scan_golden.py 采用 no-verdict 语义(+16/-6):golden 回放的 evaluate 处理从「缺 reward 记 0.0」改为「reward 缺失 / error 非空 / harness 非 canonical 都记为 ERR(reward=None + error 字段)」,避免把服务器端打分失败误记为任务的 0.0 基线;日志尾部捕获条件同步从 reward < 1.0 改为 reward is None or < 1.0。
-
配套改动:测试侧 _FakeEnv / _FakeResult 改为模拟携带 harness 标记的契约服务器,evaluate 分支直接返回 reward + info;新增 test_old_server_reward_is_not_trusted 用 _OldServerEnv 模拟旧服务器(reward=1.0 但 info 只有 {tests_passed, exit_code}),断言 reward=None 且 episode 本身跑完(turns == 2)。配置侧 OPENENV_MESSAGE_TIMEOUT_S 默认值从 600 提到 1200(evaluate 现在在单次 env 操作内跑完整 verifier 预算,官方任务最高 3600s;超时发生在完整轨迹生成之后,是最昂贵的失败点)。文档侧 README 与 openenv_launch_common.py 的预检报错把安装下限从「#965/#972 合并」改为「≥ #1012 合并(04d259ea6)」并去掉「not upstream main」这类旧表述。
关键文件:
examples/experimental/openenv/openenv_agent_function.py(模块 环境适配;类别 source;类型 core-logic;符号 _multi_turn, _obs_info, _apply_workdir, _parse_reward_marker): 本 PR 的核心:删除 adapter 侧全套补偿机制(_apply_workdir / _CANONICAL_EVAL_CMD / marker 解析器),新增 _obs_info 与 evaluate 契约守卫,并把 OPENENV_MESSAGE_TIMEOUT_S 默认值从 600s 提到 1200s。净删 162 行,是双协议统一的主战场。
examples/experimental/openenv/tests/test_openenv_agent_function.py(模块 适配器测试;类别 test;类型 test-coverage;符号 _FakeEnv, _FakeResult, _OldServerEnv, test_shared_leg_dispatch): 测试配套的核心:_FakeEnv / _FakeResult 改为模拟携带 harness 标记的契约服务器,并新增 test_old_server_reward_is_not_trusted 直接验证守卫对旧服务器的拒绝行为(reward=1.0 但无 marker → reward=None)。
examples/experimental/openenv/scan_golden.py(模块 回放脚本;类别 source;类型 core-logic;符号 scan_one): golden 回放脚本同步采用 no-verdict 语义:服务器端打分失败或 harness 非 canonical 时记为 ERR(reward=None + error 字段),而不是伪造 0.0 基线,避免把基础设施故障错误归结到任务上。
examples/experimental/openenv/openenv_daytona_agent_function.py(模块 云沙箱;类别 source;类型 core-logic;符号 run_episode, run): Daytona 腿的 run_episode 删掉 native_evaluate=True 传参及相关注释,改为与共享腿完全一致的 _multi_turn 接线;两条腿只在 run_body / post_episode 上分化。
examples/experimental/openenv/openenv_launch_common.py(模块 启动预检;类别 source;类型 core-logic;符号 apply_optional_env_vars): Daytona 腿的启动预检报错文案从「安装 pinned checkout,不要上游 main」改为「安装 ≥ #1012 合并(04d259ea6)的 checkout」,消除双协议时代的旧表述。
examples/experimental/openenv/tests/test_openenv_daytona_agent_function.py(模块 沙箱测试;类别 test;类型 test-coverage;符号 test_daytona_leg_dispatch, test_daytona_leg_eval_error_yields_no_verdict): Daytona 腿测试同步更新:断言 exec 原样透传 + evaluate 打分,并去掉守护已删除路径的 test.sh 负向断言(那是一条代码已无法产生的路径)。
examples/experimental/openenv/README.md(模块 文档说明;类别 docs;类型 documentation): 文档把共享服务器的要求写成版本契约(tbench2_env ≥ #1012 合并 04d259ea6),并说明不满足契约时 adapter 会丢弃每个 episode 并告警。
关键符号:_multi_turn, _obs_info, run_episode, test_old_server_reward_is_not_trusted, _apply_workdir (已删除), _parse_reward_marker (已删除), _parse_testsh_rc (已删除)
关键源码片段
examples/experimental/openenv/openenv_agent_function.py
本 PR 的核心:删除 adapter 侧全套补偿机制(_apply_workdir / _CANONICAL_EVAL_CMD / marker 解析器),新增 _obs_info 与 evaluate 契约守卫,并把 OPENENV_MESSAGE_TIMEOUT_S 默认值从 600s 提到 1200s。净删 162 行,是双协议统一的主战场。
def _obs_info(result: Any) -> dict:
"""读取 StepResult 上 Observation 的 info 字典(缺失时返回空 dict)。
契约守卫靠它读 evaluate 回复的 info.harness——这是判断服务器是否
满足 canonical 打分契约的运行时指纹(对远端服务器无法做源码 preflight)。
"""
obs = getattr(result, "observation", result)
return getattr(obs, "info", None) or {}
# 运行时契约守卫(_multi_turn 的 evaluate 处理段;逻辑与 scan_golden.py 完全同语义):
# 旧部署不会报错,只会每个 episode 返回一个看起来合理的(但打错分的)
# reward。所以守卫只认一个证据:evaluate 回复必须携带当前上游
# (>= OpenEnv#1012,04d259ea6)才会发射的 harness 标记;任何其它情况
# —— 旧服务端 bare-pytest 的 {tests_passed, exit_code} info、服务端打分
# error、reward 缺失 —— 一律按 " 无裁决 " 丢弃样本,而不是记一条假 0.0:
#
# info = _obs_info(res)
# if (getattr(res, "reward", None) is None
# or _obs_field(res, "error")
# or info.get("harness") != "tests/test.sh"):
# logger.warning(
# "evaluate produced no canonical verdict "
# f"(error={_obs_field(res, 'error')!r}, harness={info.get('harness')!r}); "
# "dropping episode"
# )
# reward = None # 训练包装器据此丢弃样本
# else:
# reward = float(getattr(res, "reward"))
examples/experimental/openenv/scan_golden.py
golden 回放脚本同步采用 no-verdict 语义:服务器端打分失败或 harness 非 canonical 时记为 ERR(reward=None + error 字段),而不是伪造 0.0 基线,避免把基础设施故障错误归结到任务上。
m = {
"solve_exit": solve_exit,
"solve_s": round(solve_s, 1),
"eval_s": round(time.monotonic() - t, 1),
}
# 与 agent loop 守卫相同的 no-verdict 语义:服务端打分失败
# 或 harness 非 canonical 时记为 ERR,而不是伪造一个 0.0
# 把基础设施故障错记到任务头上。
raw_reward = getattr(res, "reward", None)
eval_error = oaf._obs_field(res, "error")
harness = str(oaf._obs_info(res).get("harness", ""))
if raw_reward is None or eval_error or harness != "tests/test.sh":
m["reward"] = None
m["error"] = f"no canonical verdict (error={eval_error!r}, harness={harness!r})"
else:
m["reward"] = float(raw_reward)
if capture_logs and (m["reward"] is None or m["reward"] < 1.0):
# evaluate 输出本身携带 test.sh 日志尾部;磁盘上的
# /logs/verifier 只在 verify 窗口内存在
m["test_log_tail"] = (oaf._obs_field(res, "output") or "")[-800:]
res = await env.step(action(action_type="exec", command="tail -c 1200 /tmp/solve.log 2>&1"))
m["solve_log_tail"] = oaf._obs_field(res, "output")
return m
评论区精华
两条 review 交锋均来自 Shi-Dong 的逐行核对,质量很高:
-
0.0 vs None 的语义差异(正确性):Shi-Dong 对照 OpenEnv#1012 源码(04d259ea6)指出,被删除的 _parse_reward_marker 在 reward.txt 缺失时返回 None(丢弃样本),而上游 evaluate 在 reward.txt 缺失时给 0.0——这意味着删除补偿后,服务端 test.sh 崩溃等 infra 失败会以假 0.0 混入训练信号。nblintao 承认差异并提交 huggingface/OpenEnv#1025 修复上游。
-
超时默认值 vs 服务端 verifier 预算(性能/运维):Shi-Dong 提示 OpenEnv 默认给 verifier 900 秒,600s 的消息超时默认值会掐断合法 evaluate。nblintao 将 OPENENV_MESSAGE_TIMEOUT_S 默认值提到 1200s 并写入文档(必须超过服务端最长的 verifier budget),同时创建 OpenEnv#1026 建议两侧超时自动协商管理。
- reward.txt 缺失时的语义差异:0.0 vs None (correctness): nblintao 承认差异并提交 huggingface/OpenEnv#1025 修复上游;本 PR 的删除按计划推进,语义差由上游 follow-up 收敛。
- 消息超时默认值与服务端 verifier 预算 (performance): nblintao 将 OPENENV_MESSAGE_TIMEOUT_S 默认值提到 1200s 并写入文档(必须超过服务端最长 verifier budget),另建 OpenEnv#1026 建议两侧超时自动协商管理。
风险与影响
- 风险:
- client/server 强耦合的切换窗口:新旧双向不兼容——旧 client 的补偿逻辑在 tests 被 withheld 的新服务器上会失效,新 client 在旧服务器上会丢弃一切样本。PR body 明确要求步骤 2(服务器升级重启)与 3(本 PR 合入)由部署方同步执行,这是最大的运营风险。
-
残余静默误打分路径:Shi-Dong 指出的 reward.txt 缺失 → 0.0 语义差在 OpenEnv#1025 合入前依然存在:服务端 test.sh 异常退出且 /logs/verifier 无裁决时,上游会给 0.0 而非丢弃,假负样本仍可能进入训练数据。
-
行为变更波及自定义任务:不带 tests/test.sh 的任务目录现在一律被丢弃(之前走服务端 pytest fallback 还能打分)。官方 89 任务与合成池无影响,自定义任务集需要核对。
-
超时配置敏感:OPENENV_MESSAGE_TIMEOUT_S 默认 1200s 只保证覆盖服务端默认 900s 预算;声明更大 [verifier].timeout_sec(官方套件最高 3600s)的任务集仍需手动调大,否则会在最昂贵的时点(完整轨迹生成后)失败。
-
验证缺口:session-server TITO 接线 + GRPO 训练循环未经本 PR 的 GPU 验证(PR 明确 defer 到切换时的 4-GPU smoke),依赖 #1675 的 4×H200 结果覆盖两条腿共享的代码路径。
- 影响:
- 部署/运维:shared-server 部署必须升级 tbench2_env ≥ 04d259ea6 并重启;升级后旧 client 的补偿逻辑失效,故需与本 PR 同步上线。一个正向副作用:新版服务器把 tests/ 和 solution/ 从 /task 撤走(verifier-asset withholding),同时封掉了旧部署暴露给 agent 的 reward 作弊洞。
-
系统:agent loop 净减约 200 行,双协议并存的维护复杂度消除;奖励语义与官方 Terminal-Bench-2 对齐(服务端跑 canonical tests/test.sh、真实 WORKDIR、真实 verifier 预算)。
-
团队:与上游 huggingface/OpenEnv 的契约耦合加深,仓库内多处以「≥ #1012 合并(04d259ea6)」作为安装下限;正在进行的 OpenEnv#1025 / #1026 follow-up 会继续收紧剩余语义差。
- 风险标记:跨仓库协调部署, 旧服务端静默误打分风险, 行为变更:无 test.sh 任务被丢弃, 超时默认依赖服务端预算, GPU 端到端验证缺失
关联脉络
- PR #1675 (标题未在上下文中提供): 本 PR 的 direct 前身:引入 native_evaluate 双协议接线;PR body 明确引用其 review 线程(discussion_r3645321595)作为退役 native_evaluate=False 分支的依据,且 #1675 的 4×H200 验证覆盖了两条腿共享的 agent loop / 训练包装代码,封住了本 PR 的 GPU 验证缺口。
参与讨论