跳到主要内容

执行 harness:一次 trial 的端到端主线

30 秒导读: harness 是 Terminal-Bench 的评测引擎。给它一个任务目录(里面有指令、Docker 配置、测试脚本),它就负责把一个 AI agent 塞进真实的沙箱终端里干活,然后另起一个会话跑测试,最后解析测试输出、判定这次到底 pass 还是 fail。本章讲清这条从"任务目录"到"一条 pass/fail 结果"的中央控制流——它是整个项目所有齿轮咬合的地方。

本章的位置:01-task-anatomy 讲一个任务长什么样(数据这一半),本章讲引擎怎么消费这份数据。沙箱怎么起(Docker + tmux 的巧劲)留给 03-terminal-tmux;被评测的 agent 内部长什么样留给 04-agents;测试输出怎么解析成分数、各种指标怎么算、断点续跑的锁文件细节留给 05-scoring-results


1. 这是什么(零基础也能懂)

一句话定义: harness 是一台评测流水线的总控——输入一个任务,输出这个任务被某个 AI agent 做没做出来。

它要解决的问题。 你想公平地测"AI agent 在真实终端里到底行不行"。这件事天生麻烦,因为它不是"调一次模型看回答对不对"那么简单,而是要:

  • 给 agent 一个干净、隔离的真实 Linux 环境(不能污染你的机器,也不能让上一个任务的残留影响下一个);
  • 让 agent 在里面自由敲命令、装包、改文件,爱怎么折腾怎么折腾;
  • agent 说"我做完了"之后,用一套它碰不到的测试去验收;
  • 把整个过程录下来,好复盘"它当时到底干了啥"。

harness 干的就是把这套流程自动化、标准化、还能并发地跑几百个任务。

用起来什么样。 用户几乎不直接碰 Harness 类,而是敲一行 CLI:

# 用 terminus agent + 某个模型,跑整个数据集
tb run --agent terminus --model anthropic/claude-3-7-latest

CLI 在背后就是构造一个 Harness 对象、调它的 .run()。跑完你会在输出目录里得到一棵产物树:每个任务、每次尝试各一个文件夹,里面躺着终端画面快照、会话录像、agent 日志、还有一份 results.json

一句话直觉。 把 harness 想成一个监考 + 阅卷的机器人:它给每个考生(agent)单独开一间考场(Docker 容器)、发卷子(instruction)、全程录像、时间一到收卷、然后用标准答案(测试脚本)判分。本章就是跟着这个机器人走一遍完整的监考流程。


2. 顶层全景(它大概怎么转)

2.1 五层调用栈

harness 的主线是一条五层嵌套的调用链,从"跑一整个 benchmark"层层收窄到"跑一次 trial"。先建立这张骨架,后面每一节都是在放大其中一层。

Harness.run() # 一整个 benchmark run(写元数据+锁 → 跑 → 收尾上传)
└─ _execute_tasks() # 并发调度:线程池铺开 所有任务 × n_attempts
└─ _execute_single_trial() # 一个 trial 的外壳:建 TrialHandler、兜底 try/except、落 results.json
└─ _run_trial() # ★ 端到端主线:起容器→agent→测试→解析→判定
└─ _run_agent() # 把 agent.perform_task 包进 asyncio 超时里跑

术语先点破:一次 trial(试次) = "某个任务的第 k 次尝试"。同一个任务可以跑 n_attempts 次(为了算 pass@k 这种指标),每一次就是一个独立 trial,有独立的容器、独立的产物目录。

2.2 各层职责一句话

方法干什么位置
L1 总控Harness.run非续跑时写 run 元数据 + 锁文件,跑任务,收尾上传harness/harness.py:1226
L2 调度_execute_tasksThreadPoolExecutor 并发跑 len(dataset) × n_attempts 个 trial,边完成边写聚合结果harness/harness.py:1099
L3 外壳_execute_single_trialTrialHandler,调 _run_trial,把返回结果写进该 trial 的 results.json;异常兜底成 UNKNOWN_AGENT_ERRORharness/harness.py:991
L4 主线_run_trial本章主角:起容器 → agent → 测试 → 解析 → 判定,全程写快照harness/harness.py:703
L5 跑 agent_run_agentasyncio.wait_foragent.perform_task 套超时,把各种异常翻译成 FailureModeharness/harness.py:633

2.3 一次 trial 的主线(高层,先不进代码)

_run_trial 是整台机器的中心。它的时序是一条直线,读者先记住这七步:

┌──────────────── 一个 Docker 容器的生命周期 ────────────────┐
输入:任务目录 ──▶ ①起沙箱 ──▶ ②开 agent 会话 ──▶ ③放 agent 干活 ──▶ ④(按需)另起 tests 会话
│ spin_up create_session _run_agent create_session
│ _terminal ("agent") (超时包裹) ("tests")
│ │
│ ⑤跑测试 ◀───────────────────────┘
│ _run_tests
└────────────────────────────│──────────────────────────────┘
▼ 容器销毁后才解析
⑥解析测试输出 ──▶ ⑦判定 pass/fail ──▶ 输出:一条 TrialResults
_parse_results _is_resolved

怎么读这张图: 从左到右是时间顺序。①~⑤都发生在同一个容器还活着的时候(with spin_up_terminal(...) as terminal: 块内);⑥⑦在容器已经关掉之后才做——因为解析只需要第⑤步抓下来的那份终端文本,不再需要容器。

一个关键设计先在这里点出:agent 用的会话和跑测试的会话,默认是两个不同的 tmux 会话、甚至不同的用户身份(agent 是配置用户,tests 是 root)。这样 agent 在自己 shell 里设的环境变量、别名不会污染测试环境——除非任务显式要求 run_tests_in_same_shell。这一点第 3.4 节展开。


3. 核心原理(逐个机制,由浅入深)

3.1 并发调度:线程池铺开 任务 × 尝试

要解决的小问题。 数据集可能几百个任务,每个还要跑 n_attempts 次,串行跑要跑到天荒地老。但每个 trial 又是重量级、相互独立的(各自一个 Docker 容器)。

思路。 用一个线程池,把"每个任务 × 每次尝试"都提交成一个 future,最多同时跑 n_concurrent_trials 个。为什么用线程池而不是进程池?因为每个 trial 的重活(起容器、跑命令)都是 I/O 等待型——真正干活的是 Docker 和子进程,Python 这边基本在等,线程足够,还省去了跨进程传对象的麻烦。

并发的形状:

dataset = [taskA, taskB, taskC] n_attempts = 2 n_concurrent_trials = 4

提交 3×2 = 6 个 future 进线程池:
A.1 A.2 B.1 B.2 C.1 C.2
└────┴────┴──┐ 最多 4 个同时在跑,其余排队

ThreadPoolExecutor(max_workers=min(len(dataset), n_concurrent_trials))
│ as_completed:谁先跑完先收谁

每收到一个结果 → 去重/替换 → append 到 BenchmarkResults → 立刻 _write_results 落盘

真实实现。 双重循环提交 future,外层任务、内层尝试:

# harness/harness.py:1125 _execute_tasks
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_task = {}
for task_path in self._dataset:
for attempt in range(1, self._n_attempts + 1):
trial_name = self._get_trial_name(task_path, attempt)
future = executor.submit(
self._execute_single_trial,
trial_name=trial_name,
task_path=task_path,
)
future_to_task[future] = (trial_name, attempt)

max_workers = min(len(self._dataset), self._n_concurrent_trials)harness.py:1123)——任务比并发数还少时就不浪费线程。

两个易错细节:

  • worker 数按任务数算,不按 trial 数算。 上限是 len(dataset),不是 len(dataset) × n_attempts。所以就算你把 n_attempts 调很大,同时在跑的 trial 数仍受任务数封顶。
  • 每完成一个就立刻落盘。 as_completed 每吐一个结果,就 append_write_resultsharness.py:1180)。所以 results.json增量长大的——中途 Ctrl-C 也留得下已完成的部分,这正是续跑能接上的基础。收结果时还做了去重/替换harness.py:1163):按 (task_id, trial_name) 判重,重复就覆盖而非追加,防止续跑场景下同一 trial 出现两份。

3.2 起沙箱 + 抓 agent 前画面

要解决的小问题。 每个 trial 都要一个全新、隔离的终端环境,用完即毁,还得把过程录下来。

思路。 用一个 with 上下文管理器 spin_up_terminal 把"起容器"和"关容器"锁成一对——不管中间成功失败,退出 with 块一定会 terminal.stop()(见 terminal/terminal.py:175-179try/finally)。容器起来后,先开一个名叫 "agent" 的 tmux 会话,并趁 agent 还没动手,抓一张终端画面快照存成 pre-agent.txt——留作复盘基线。

真实实现。 主线开头这一段:

# harness/harness.py:717 _run_trial
with spin_up_terminal(
client_container_name=trial_handler.client_container_name,
client_image_name=trial_handler.client_image_name,
docker_image_name_prefix=trial_handler.docker_image_name_prefix,
docker_compose_path=trial_handler.task_paths.docker_compose_path,
...
disable_recording=trial_handler.task.disable_asciinema,
) as terminal:
session = terminal.create_session(
"agent", is_active_stream=self._livestream, as_configured_user=True
)

pre_agent_pane = session.capture_pane(capture_entire=True)
trial_handler.trial_paths.pre_agent_pane_path.write_text(pre_agent_pane)

几个要点:

  • 容器名、镜像名从哪来:全由 TrialHandler 算好(第 4 节),比如客户端容器名就是 trial 名把 . 换成 -
  • create_session("agent", as_configured_user=True):agent 会话用任务配置里指定的用户跑(terminal.py:73-76:读容器 Config.User),模拟真实用户而非 root。
  • capture_pane(capture_entire=True) 抓的是整屏历史,不只当前可见部分(terminal/tmux_session.py:317)。
  • tmux 会话到底怎么把命令送进容器、怎么录像,是 03-terminal-tmux 的内容,这里不展开。

3.3 放 agent 干活:超时包裹 + 异常翻译

要解决的小问题。 agent 是被评测的、不可信的外部代码,可能:卡死不返回、跑太久、模型报"上下文超长"、解析响应失败……harness 不能被它拖垮,还得把每种死法归类成一个标准的失败码,好统计。

思路。 两层包裹:

  1. 超时层:把同步的 agent.perform_task(...) 丢进 executor 变成一个 awaitable,再用 asyncio.wait_for(..., timeout) 套上硬超时。
  2. 翻译层:外面一圈 try/except,把 asyncio.TimeoutErrorRetryError(内含各种 LLM 错误)、以及任何兜底 Exception,逐一映射成一个 FailureMode 枚举值。

超时怎么套(教学示意):

# 示意,非源码:把"同步阻塞的 agent 调用"变成"能被超时打断的异步任务"
loop = asyncio.get_event_loop()
task = loop.run_in_executor(None, lambda: agent.perform_task(...)) # 丢到线程里跑
result = await asyncio.wait_for(task, timeout=timeout_sec) # 到点就抛 TimeoutError

对应真实代码在 _run_agent_with_timeoutharness/harness.py:613-631)。超时时长的算法:优先用全局 --global-agent-timeout-sec,否则用任务自己声明的 max_agent_timeout_sec 再乘全局倍率(harness.py:639-645)。

异常 → 失败码 的翻译表_run_agentharness.py:633-701):

捕获到的情况翻译成的 FailureMode说明
agent.perform_task 正常返回,但结果为 NoneUNKNOWN_AGENT_ERRORagent 没给出结果
正常返回result.failure_modeagent 自报的状态(通常 NONE
asyncio.TimeoutErrorAGENT_TIMEOUT超时,但仍构造一个带 markers 的 partial 结果
RetryError 内含 ContextLengthExceededErrorCONTEXT_LENGTH_EXCEEDED上下文超长
RetryError 内含 OutputLengthExceededErrorOUTPUT_LENGTH_EXCEEDED输出超长
RetryError 内含 ParseErrorFATAL_LLM_PARSE_ERRORLLM 响应解析失败
其它任何 ExceptionUNKNOWN_AGENT_ERROR兜底

FailureMode 全部取值见 agents/failure_mode.py:4。)

一个关键的"仁慈"设计:agent 超时 ≠ 直接判负。 看主线里超时后的处理:

# harness/harness.py:752 _run_trial
if agent_failure_mode == FailureMode.AGENT_TIMEOUT:
results.failure_mode = agent_failure_mode
self._logger.debug(
f"Agent failed with mode {agent_failure_mode}, continuing"
" with test execution"
)
elif agent_failure_mode != FailureMode.NONE:
results.failure_mode = agent_failure_mode

注意:agent 超时时,harness 记下失败码但不 return,继续往下跑测试。理由很实在——agent 可能在被掐断前其实已经把活干完了,那就该让它拿到分。而其它类型的失败也只是先记下 failure_mode,主线并不在这里中断(真正提前 return 的只有后面测试阶段的失败,见 3.6)。agent 若给了结果,还会把 token 用量记进 resultsharness.py:762-764)。

之后再抓一张画面 post-agent.txtharness.py:749)——这张 + 前面的 pre-agent.txt 一夹,就是"agent 到底在终端里干了啥"的完整可视记录。

3.4 测试会话:默认另起一个 shell

要解决的小问题。 验收测试必须公正——不能被 agent 在自己 shell 里留下的环境变量、别名、当前目录、临时 export 之类污染

思路。 默认新开一个名叫 "tests" 的 tmux 会话来跑测试,而且用 root 身份,跟 agent 的会话彻底隔开。只有当任务显式设了 run_tests_in_same_shell = True(比如它就是要测"agent 在 shell 里设的某个变量还在不在"这种 shell 作用域的东西),才复用 agent 那个会话。

# harness/harness.py:766 _run_trial
if not trial_handler.task.run_tests_in_same_shell:
session = terminal.create_session(
"tests", is_active_stream=self._livestream, as_configured_user=False
)

对比两条路径:

场景会话用户身份适用
默认新开 "tests" 会话root(as_configured_user=False绝大多数任务,要干净环境
run_tests_in_same_shell=True复用 "agent" 会话配置用户测 shell 作用域属性(变量/别名/cwd)

run_tests_in_same_shell 是任务在 task.yaml 里声明的字段,默认 Falsehandlers/trial_handler.py:63)。

3.5 跑测试:拷进测试文件,执行 run-tests.sh

要解决的小问题。 测试脚本和测试用例文件在宿主机的任务目录里,容器里没有——得先送进去,再在容器里执行。

思路两步走:

第一步,_setup_test_env 把测试文件拷进容器harness.py:544)。要拷的是 run-tests.sh,外加 tests/ 目录(如果存在),目标是容器里的固定挂载点 /testsCONTAINER_TEST_DIRterminal/docker_compose_manager.py:30):

# harness/harness.py:544 _setup_test_env
paths = [trial_handler.task_paths.run_tests_path]
if trial_handler.task_paths.test_dir.exists():
paths.append(trial_handler.task_paths.test_dir)
terminal.copy_to_container(
paths=paths,
container_dir=str(DockerComposeManager.CONTAINER_TEST_DIR), # "/tests"
)

为什么测试文件不在起容器时就拷进去、而要等 agent 干完再拷?为了防作弊 / 防信息泄露:agent 干活期间容器里根本没有测试脚本,它没法偷看答案、也没法针对测试打补丁。

第二步,_run_tests 在测试会话里 bash /tests/run-tests.sh,同样套一个测试超时(harness.py:557):

# harness/harness.py:573 _run_tests
session.send_keys(
["bash ", str(DockerComposeManager.CONTAINER_TEST_DIR / run_tests_name), "Enter"],
block=True,
max_timeout_sec=test_timeout_sec,
)

block=True 表示阻塞等到命令跑完(或超时)。超时的话捕获 TimeoutError,返回 FailureMode.TEST_TIMEOUT。测试超时时长同样是"全局值优先,否则任务的 max_test_timeout_sec × 倍率"(harness.py:565-571)。

跑完再抓一张 post-test.txtharness.py:781)——这张终端画面就是解析器的原料:测试框架(默认 pytest)的输出全在里面。

3.6 解析 + 判定:从终端文本到 pass/fail

要解决的小问题。 测试跑完,终端上是一大片 pytest 输出文本。要把它变成结构化的"哪个用例 PASS/FAIL",再据此下最终结论。

关键顺序:解析发生在容器关掉之后。 注意 _parse_results 的调用在 with spin_up_terminal(...)外面harness.py:808,缩进已退回)——因为解析只吃 post_test_pane 那段文本,不再需要容器,早点关容器省资源。

但在关容器前,有两道提前退出的闸门(都在 with 块内):

# harness/harness.py:800 _run_trial —— 测试阶段就失败,直接返回,不再解析
if (test_failure_mode != FailureMode.NONE
and results.failure_mode == FailureMode.UNSET):
results.failure_mode = test_failure_mode
results.trial_ended_at = ...
return results

理解这个 and 条件:只有当测试环节自己出了错(如 TEST_TIMEOUT且前面还没记过别的失败码failure_mode 仍是初始的 UNSET)时,才把测试失败码定案并提前返回。换句话说,前面 agent 阶段已记下的失败不会被测试阶段覆盖,且不会走到解析。

过了闸门,_parse_results 用任务指定的 parser 去解析那段画面文本(harness.py:597):

# harness/harness.py:597 _parse_results
try:
return trial_handler.parser.parse(post_test_pane), FailureMode.NONE
except Exception as e:
self._logger.error(...) # 解析炸了 → 提示去看 post-test 快照
return None, FailureMode.PARSE_ERROR

解析成功后,_is_resolved 下最终结论——必须每一个测试用例都 PASSED,这个 trial 才算解决

# harness/harness.py:536 _is_resolved
def _is_resolved(self, parser_results) -> bool:
if parser_results is None:
return False
return all(result == UnitTestStatus.PASSED for result in parser_results.values())

结果写进 results.is_resolvedharness.py:819)。这个布尔值就是本章一路追下来的终点——一条 trial 的 pass/fail。至此 _run_trial 返回一个 TrialResults,里面塞满了时间戳、token 用量、失败码、逐用例状态、是否 resolved。指标(accuracy、pass@k)怎么从一堆 TrialResults 汇总出来,是 05-scoring-results 的事。


4. TrialHandler / TrialPaths:产物目录与命名

_run_trial 里到处在读 trial_handler.xxx——这个 TrialHandler 就是**把"一个任务目录"翻译成"这次 trial 需要的一切路径、配置、命名"**的适配器。它在 L3 外壳里被构造(harness.py:1004)。

4.1 TrialHandler 提供什么

构造时它做三件事(handlers/trial_handler.py:238-254):读并校验 task.yaml 成一个 Task 对象、按任务声明选好 parser、并(若给了 output_path)建好这次 trial 的输出目录树。

它对外暴露的关键属性:

属性用途
task_id任务目录名(task_paths.input_path.name结果归属、目录分组
instructiontask.instruction喂给 agent 的题面
taskTask 对象读各种超时、run_tests_in_same_shelldisable_asciinema
parserparser 实例解析测试输出
docker_image_name_prefixtb__{task_id}.-镜像命名
client_container_nametrial_name.-容器命名
client_image_name{prefix}__client客户端镜像名

(依据:handlers/trial_handler.py:256-274。为什么把 . 换成 -?Docker 命名不喜欢点号。)

4.2 trial 命名规则

trial 名由 _get_trial_name 拼出(harness.py:1032):

{task_id}.{attempt}-of-{n_attempts}.{run_id}
例: hello-world.1-of-3.2026-07-24__10-30-00

三段信息一眼可读:哪个任务、第几次尝试(共几次)、属于哪个 run。续跑时正是靠重算同名 trial 来判断"这次尝试做没做过"(harness.py:328)。

4.3 产物目录结构

TrialPathshandlers/trial_handler.py:172)规定了每个 trial 落盘长什么样。构造 TrialHandler 时若带了 output_path 就自动 mkdir 建好骨架(trial_handler.py:231-235):

output_path/ ← --output-path 下的 {run_id}/
└── {task_id}/
└── {trial_name}/
├── panes/ ← 三张终端画面快照
│ ├── pre-agent.txt · agent 动手前
│ ├── post-agent.txt · agent 干完后
│ └── post-test.txt · 测试跑完后(解析的原料)
├── sessions/ ← tmux 会话数据 + agent.cast 录像
├── agent-logs/ ← agent 自己的日志目录
├── commands.txt ← 命令历史
└── results.json ← 这一条 trial 的最终结果(单一真相源)

这套结构对应属性:pre/post_agent_pane_pathpost_test_pane_pathsessions_pathagent_logging_dircommands_pathresults_pathtrial_handler.py:207-229)。

一个重要的"单一真相源"设计: 每个 trial 的 results.json 是权威,run 根目录那个聚合 results.json 只是把它们拼起来。续跑时正是靠扫每个 trial 目录里有没有 results.json 来判断"这次尝试完成没有"(harness.py:1039 _load_previous_resultsharness.py:332)。


5. 断点续跑(点到即止)

harness 支持中断后接着跑,机制的骨架就穿插在上面的主线里,这里只勾勒,锁文件的字段细节留给 05-scoring-results

三个关键动作:

  • 认出在续跑:构造时若输出目录已存在,_is_resuming = Trueharness.py:154),并校验当前配置和已存的 tb.lock 一致,不一致就报错拒绝续跑(_validate_resume_configurationharness.py:200)。
  • 跳过已完成、清理半成品_filter_completed_and_cleanup_incomplete_tasksharness.py:296)遍历任务,某任务的所有 attempt 都有 results.json 才算完成、从待跑列表剔除;有残缺产物的任务目录整个删掉,防止脏数据(harness.py:365 _clean_incomplete_task_artifacts)。
  • 载入旧结果_load_previous_resultsharness.py:1039)从各 trial 目录把 TrialResults 读回来,作为本次聚合结果的起点,新跑的再往上加(并去重)。

一句话记住这条设计脉络:每 trial 独立的 results.json + 一份运行锁,让续跑既安全(配置不匹配就拒绝)又精准(只补没做完的)。


6. 巧妙之处(可借鉴)

  • 测试文件"迟到"进容器。 agent 干活期间容器里没有测试脚本,直到 agent 结束才 copy_to_containerharness.py:544)——从物理上杜绝 agent 偷看/针对测试,比"靠提示词请求别作弊"可靠得多。
  • agent 超时也给机会跑测试。 超时只记 failure_mode 不 return(harness.py:752),万一它掐断前已做完,仍能 resolved。评测的仁慈来自"以结果论"而非"以是否守时论"。
  • 失败码不被覆盖的优先级。 测试阶段的失败只在 failure_mode == UNSET 时才定案(harness.py:800),保证 agent 阶段更早、更根因的失败信息不被后面的现象级失败盖掉。
  • 异常翻译成枚举。 把五花八门的 Python 异常(超时/上下文超长/输出超长/解析失败/未知)统一映射成 FailureModeharness.py:633),让"为什么没做出来"变成可统计、可对比的结构化字段。
  • 每完成即落盘 + 去重替换。 as_completed 循环里每来一个结果就写 results.json,并按 (task_id, trial_name) 去重覆盖(harness.py:1163)——崩了能续、重跑不会重复计数。
  • 单一真相源。 聚合结果永远可从各 trial 的 results.json 重建(harness.py:1039),聚合文件坏了不致命。

7. 边界与局限

  • 依赖 Docker。 整条主线建立在能起 Docker 容器上;无 Docker 环境跑不了。
  • 解析靠抓终端画面文本,不靠退出码。 判定来自对 post-test.txt 的文本解析(harness.py:808),所以测试脚本的输出格式必须匹配所选 parser,否则 PARSE_ERROR。测试输出被截断/花屏也会误判。
  • 线程并发受 GIL 与机器资源限制。 用线程池是因为活儿是 I/O 型;但 n_concurrent_trials 开太大会被 Docker/内存/CPU 拖垮,且 worker 上限被任务数封顶(harness.py:1123)。
  • 续跑要求配置严格一致。 换了 agent/模型/并发数等再想续跑,锁校验会直接拒绝(harness.py:243),只能新开 run。
  • _execute_single_trial 的兜底会吞掉根因。 trial 里任何未预料异常都被统一记成 UNKNOWN_AGENT_ERRORharness.py:1019-1030),细节只在日志里,results.json 里看不出具体原因。

8. 代码地图(导航索引)

主题文件路径符号
总控入口(写元数据/锁 → 跑 → 上传)terminal_bench/harness/harness.pyHarness.run
并发调度(线程池 × n_attempts、增量落盘)terminal_bench/harness/harness.py_execute_tasks
单 trial 外壳(建 handler、兜底、写 results.json)terminal_bench/harness/harness.py_execute_single_trial
端到端主线terminal_bench/harness/harness.py_run_trial
跑 agent + 异常翻译成 FailureModeterminal_bench/harness/harness.py_run_agent
agent 超时包裹(asyncio.wait_for)terminal_bench/harness/harness.py_run_agent_with_timeout
拷测试文件进容器terminal_bench/harness/harness.py_setup_test_env
执行 run-tests.sh + 测试超时terminal_bench/harness/harness.py_run_tests
解析测试画面文本terminal_bench/harness/harness.py_parse_results
判定 pass/fail(全 PASSED 才算)terminal_bench/harness/harness.py_is_resolved
trial 命名规则terminal_bench/harness/harness.py_get_trial_name
续跑:跳过已完成/清理半成品terminal_bench/harness/harness.py_filter_completed_and_cleanup_incomplete_tasks
续跑:载入旧结果terminal_bench/harness/harness.py_load_previous_results
trial 适配器(路径/配置/命名)terminal_bench/handlers/trial_handler.pyTrialHandler
输入任务目录的路径terminal_bench/handlers/trial_handler.pyTaskPaths
输出产物目录结构terminal_bench/handlers/trial_handler.pyTrialPaths
任务配置模型(超时、run_tests_in_same_shell 等)terminal_bench/handlers/trial_handler.pyTask
起/关容器上下文管理器terminal_bench/terminal/terminal.pyspin_up_terminal
建 tmux 会话(agent/tests)terminal_bench/terminal/terminal.pyTerminal.create_session
抓终端画面快照terminal_bench/terminal/tmux_session.pycapture_pane
容器内测试目录常量 /teststerminal_bench/terminal/docker_compose_manager.pyCONTAINER_TEST_DIR
失败码枚举terminal_bench/agents/failure_mode.pyFailureMode
单/聚合结果模型、指标terminal_bench/harness/models.pyTrialResults / BenchmarkResults