RealReplicaBench
§ 00 · Source-level Observatory

RealReplicaBench 真实副本 Agent Benchmark

RealReplicaBench 不是又一个静态问答集,而是把 Gmail、Shopify、Stripe、Alibaba、Google Trends、文件任务和 CLI mock 都包装成可复现实站副本,用来测长程 Agent 是否能在有状态环境里完成业务闭环。

★ 1,036
GitHub Stars
3.1K
Files
107
Tasks
2026-08-06
Last commit
collection.json → load_task(task_id) → TaskSpec → public workspace copy + runtime mocks → OpenClaw / Accio harness run → verifier reward.json + trajectory.json → binary final reward + HTML report
§ 00 · OVERVIEW

它解决什么,为什么值得拆

这个项目的核心价值在于把 benchmark 从“给模型一段题面”推进到“给 Agent 一个可运行工作场景”。入口脚本在 scripts/run_realreplicabench.py:22 找到仓库根目录,在 scripts/run_realreplicabench.py:34 约定 runs 输出目录,在 scripts/run_realreplicabench.py:35 绑定完整 collection;这三处说明它把任务集、运行产物和批量执行器作为同一个发布单元维护,而不是散落的示例脚本。

真正的执行契约落在 TaskSpec:src/real_replica_bench/cli.py:328、src/real_replica_bench/cli.py:340、src/real_replica_bench/cli.py:358、src/real_replica_bench/cli.py:415、src/real_replica_bench/cli.py:481、src/real_replica_bench/cli.py:494。这些行把 task.toml、runtime_mocks、browser/vision 标签、verifier 超时和输出目录串起来,意味着每个任务不仅有自然语言描述,还有可判定的环境需求与评分入口。对长程 Agent 来说,这比单次 API benchmark 更接近生产故障排查和后台办公自动化。

▸核心数据

架构模式
Dataset collection → TaskSpec → Docker runtime → harness → verifier → report。
核心抽象
TaskSpec、RuntimeMockConfig、binary reward、trajectory recovery。
性能/精度数据
发布校验固定 107 个任务,并按 text/browser/vision 切 collection。
下游依赖
Docker/OpenClaw、mock service、LLM judge provider configs。
§ 01 · ARCHITECTURE

从 collection 到 TaskSpec 的装配链

▸任务发现不是文件名匹配

加载流程先在 src/real_replica_bench/cli.py:328 进入 load_task,再在 src/real_replica_bench/cli.py:331 检查直接路径,如果没有命中就从 src/real_replica_bench/cli.py:332 开始递归扫描 task.toml,并用 src/real_replica_bench/cli.py:334 的 task.id 匹配。这让 domain-organized 数据目录可以重排,而外部调用仍稳定使用任务 id。

src/real_replica_bench/cli.py · L328-358Python
328def load_task(task_id: str) -> TaskSpec:
329    case_dir = DATASETS_DIR / task_id
330    task_toml = case_dir / "task.toml"
331    if not task_toml.is_file():
332        for candidate in DATASETS_DIR.rglob("task.toml"):
333            cdata = tomllib.loads(candidate.read_text(encoding="utf-8"))
334            if cdata.get("task", {}).get("id") == task_id:
335                case_dir = candidate.parent
336                task_toml = candidate
337                break
338        else:
339            raise SystemExit(f"task not found: {task_id} (searched {DATASETS_DIR})")
340    data = tomllib.loads(task_toml.read_text(encoding="utf-8"))
341    task = data.get("task", {})
342    agent = data.get("agent", {})
343    verifier = data.get("verifier", {})
344    environment = data.get("environment", {})
345    host_port_raw = environment.get("host_published_port")
346    host_published_port: int | None = None
347    if host_port_raw is not None:
348        try:
349            host_published_port = int(host_port_raw)
350        except (TypeError, ValueError):
351            raise SystemExit(
352                f"task {task_id}: [environment].host_published_port must be an integer, got {host_port_raw!r}"
353            )
354        if host_published_port <= 0 or host_published_port > 65535:
355            raise SystemExit(
356                f"task {task_id}: [environment].host_published_port out of range: {host_published_port}"
357            )
358    runtime_mocks = _parse_runtime_mocks(task_id, environment)

▸环境声明会提升为运行时能力

runtime mocks 在 src/real_replica_bench/cli.py:358 解析,端口在 src/real_replica_bench/cli.py:360 收集,隐式端口规则在 src/real_replica_bench/cli.py:361 到 src/real_replica_bench/cli.py:362 生效;这些判断决定任务是否能暴露 HTTP/CLI mock,也决定 early terminate 是否有可轮询服务。

src/real_replica_bench/cli.py · L358-434Python
358    runtime_mocks = _parse_runtime_mocks(task_id, environment)
359    implicit_host_port = None
360    runtime_mock_ports = [mock.port for mock in runtime_mocks if mock.port is not None]
361    if host_published_port is None and len(runtime_mock_ports) == 1:
362        implicit_host_port = runtime_mock_ports[0]
363    early_terminate: EarlyTerminate | None = None
364    et_raw = environment.get("early_terminate")
365    if et_raw is not None:
366        if not isinstance(et_raw, dict):
367            raise SystemExit(
368                f"task {task_id}: [environment.early_terminate] must be a table"
369            )
370        required = ("poll_path", "match_kind", "match_field", "match_value")
371        for key in required:
372            if key not in et_raw:
373                raise SystemExit(
374                    f"task {task_id}: [environment.early_terminate].{key} is required"
375                )
376        match_kind = str(et_raw["match_kind"])
377        if match_kind != "list_any_field_equals":
378            raise SystemExit(
379                f"task {task_id}: unsupported early_terminate.match_kind {match_kind!r}"
380            )
381        if host_published_port is None and implicit_host_port is None:
382            raise SystemExit(
383                f"task {task_id}: early_terminate requires host_published_port or exactly one runtime mock port"
384            )
385        early_terminate = EarlyTerminate(
386            poll_path=str(et_raw["poll_path"]),
387            poll_interval_sec=float(et_raw.get("poll_interval_sec", 5)),
388            match_kind=match_kind,
389            match_field=str(et_raw["match_field"]),
390            match_value=str(et_raw["match_value"]),
391            kill_pattern=str(et_raw.get("kill_pattern", "bun run runner.ts")),
392        )
393    output_file_early_terminate: OutputFileEarlyTerminate | None = None
394    output_et_raw = environment.get("output_file_early_terminate")
395    if output_et_raw is not None:
396        if not isinstance(output_et_raw, dict):
397            raise SystemExit(
398                f"task {task_id}: [environment.output_file_early_terminate] must be a table"
399            )
400        output_path = str(output_et_raw.get("output_path") or "")
401        if not output_path.startswith("/"):
402            raise SystemExit(
403                f"task {task_id}: [environment.output_file_early_terminate].output_path must be an absolute path"
404            )
405        output_file_early_terminate = OutputFileEarlyTerminate(
406            output_path=output_path,
407            poll_interval_sec=float(output_et_raw.get("poll_interval_sec", 2)),
408            kill_pattern=str(output_et_raw.get("kill_pattern", "bun run runner.ts")),
409        )
410    # Layout-aware default for the verifier entrypoint: the new layout uses
411    # grader/run.sh, the old (frozen v2) layout uses test.sh. The task prompt file
412    # is `task.md` in BOTH layouts. An explicit [task].entrypoint / [task].test in
413    # task.toml always wins over the detected default.
414    test_default = "grader/run.sh" if (case_dir / "grader" / "run.sh").is_file() else "test.sh"
415    return TaskSpec(
416        task_id=str(task.get("id") or task_id),
417        case_dir=case_dir,
418        name=str(task.get("name") or task_id),
419        language=str(task.get("language") or "zh-CN"),
420        timeout_sec=int(agent.get("timeout_sec", 900)),
421        max_actions=int(agent.get("max_actions", 6)),
422        verifier_timeout_sec=int(verifier.get("timeout_sec", 120)),
423        entrypoint=str(task.get("entrypoint") or "task.md"),
424        test_script=str(task.get("test") or test_default),
425        rubric=str(task.get("rubric") or "rubric.json"),
426        private_dir=(case_dir / "private") if (case_dir / "private").is_dir() else None,
427        mock_services=bool(environment.get("mock_services", False) or runtime_mocks),
428        host_published_port=host_published_port,
429        requires_browser=bool(environment.get("requires_browser", False)),
430        requires_vision=bool(environment.get("requires_vision", False)),
431        runtime_mocks=runtime_mocks,
432        early_terminate=early_terminate,
433        output_file_early_terminate=output_file_early_terminate,
434    )

▸公开输入和评分材料被硬隔离

src/real_replica_bench/cli.py:481 到 src/real_replica_bench/cli.py:510 是安全边界:新布局只复制 workspace 和 task.md,grader、services、private、rubric、task.toml 都不进入 agent 可见目录。这个实现直接影响 benchmark 可信度,因为 Agent 不能靠读答案文件拿分。

src/real_replica_bench/cli.py · L481-539Python
481def copy_public_task_dir(src: Path, dst: Path) -> None:
482    """Copy the agent-visible task content, never exposing verifier-only data.
483
484    New layout (allowlist): the agent sees ONLY ``workspace/`` (its inputs) plus
485    the task statement ``task.md``. Everything verifier-only — ``grader/``,
486    ``services/`` (mock source), ``private/``, ``rubric.json``, ``task.toml`` —
487    lives outside ``workspace/`` and is structurally excluded by not copying it.
488    This replaces the old blocklist, where a mock/verifier filename missing from
489    the ignore tuple would leak answers to the agent.
490
491    Old layout (frozen v2, no ``grader/``/``workspace/``): fall back to the
492    historical blocklist over the whole task dir. Removed once v2 migrates (Phase 2).
493    """
494    workspace = src / "workspace"
495    # Detect the new layout by grader/ or workspace/ so browse-only tasks (no
496    # agent inputs → no workspace/) still take the allowlist path instead of
497    # falling through to the old blocklist (which would leak grader/ + services/).
498    is_new_layout = (src / "grader").is_dir() or workspace.is_dir()
499    if is_new_layout:
500        dst.mkdir(parents=True, exist_ok=True)
501        if workspace.is_dir():
502            shutil.copytree(
503                workspace,
504                dst / "workspace",
505                ignore=shutil.ignore_patterns(".DS_Store", "__pycache__", "*.pyc"),
506            )
507        task_md = src / "task.md"
508        if task_md.is_file():
509            shutil.copy2(task_md, dst / "task.md")
510        return
511    ignore = shutil.ignore_patterns(
512        ".DS_Store",
513        "fixtures",
514        "attachments",
515        "node_modules",
516        "private",
517        "__pycache__",
518        "*.pyc",
519        "rubric.json",
520        "test.sh",
521        "task.toml",
522        "source_metadata.json",
523        "REFERENCE_SOLUTION.py",
524        "_grader.py",
525        "host_deps.txt",
526        "package.json",
527        "package-lock.json",
528        "start_mock_services.sh",
529        "mock*.py",
530        "mock*.js",
531        "*server.py",
532        "*server.js",
533        "*.mjs",
534        "verify*.py",
535        "INPUTS_TO_BUILD.md",
536        "ASSET_INVENTORY.md",
537        "EXPECTED_BEHAVIOR.md",
538    )
539    shutil.copytree(src, dst, ignore=ignore)
深度阅读 · Harness 装配链
§ 02 · CORE MODULES

核心模块:运行器、mock、评分

▸批量运行脚本保留轻量配置解析

scripts/run_realreplicabench.py:48 到 scripts/run_realreplicabench.py:79 手写 scalar parser,scripts/run_realreplicabench.py:82 到 scripts/run_realreplicabench.py:138 解析 YAML 子集。这不是通用 YAML,而是为了 run config 在未安装依赖时仍能被 release 脚本和 smoke 命令读取。

src/scripts/run_realreplicabench.py · L48-138Python
48def parse_scalar(value: str) -> Any:
49    value = value.strip()
50    # Strip inline comments (` # ...`) outside of quotes. YAML treats `#`
51    # preceded by whitespace as a comment; without this our custom subset
52    # parser silently glued the comment onto the value, breaking numeric coercion.
53    if value and not (value.startswith('"') or value.startswith("'")):
54        hash_idx = value.find(" #")
55        if hash_idx != -1:
56            value = value[:hash_idx].rstrip()
57    if value in {"", "null", "~"}:
58        return None
59    if value in {"true", "True"}:
60        return True
61    if value in {"false", "False"}:
62        return False
63    if value.startswith('"') and value.endswith('"'):
64        return value[1:-1].replace('\\"', '"')
65    if value.startswith("'") and value.endswith("'"):
66        return value[1:-1].replace("''", "'")
67    if value.startswith("[") and value.endswith("]"):
68        inner = value[1:-1].strip()
69        if not inner:
70            return []
71        return [parse_scalar(part.strip()) for part in inner.split(",")]
72    try:
73        return int(value)
74    except ValueError:
75        pass
76    try:
77        return float(value)
78    except ValueError:
79        return value
80
81
82def load_yaml_subset(path: Path) -> dict[str, Any]:
83    if not path.is_file():
84        raise SystemExit(f"config file not found: {path}")
85    root: dict[str, Any] = {}
86    stack: list[tuple[int, Any]] = [(-1, root)]
87    last_key_at_indent: dict[int, tuple[Any, str]] = {}
88    for raw_line in path.read_text(encoding="utf-8").splitlines():
89        if not raw_line.strip() or raw_line.lstrip().startswith("#"):
90            continue
91        indent = len(raw_line) - len(raw_line.lstrip(" "))
92        line = raw_line.strip()
93        parent = stack[-1][1]
94        if line.startswith("- "):
95            while stack and indent < stack[-1][0]:
96                stack.pop()
97            parent = stack[-1][1]
98            if not isinstance(parent, list):
99                grand_parent, key = last_key_at_indent.get(indent, (None, None))
100                if grand_parent is None:
101                    raise SystemExit(f"invalid yaml list item: {raw_line}")
102                if isinstance(grand_parent.get(key), list):
103                    parent = grand_parent[key]
104                else:
105                    parent = []
106                    grand_parent[key] = parent
107                if not stack or stack[-1][1] is not parent:
108                    stack.append((indent, parent))
109            item_text = line[2:].strip()
110            if not item_text:
111                item: Any = {}
112                parent.append(item)
113                stack.append((indent, item))
114            elif ": " in item_text:
115                key, value = item_text.split(":", 1)
116                item = {key.strip(): parse_scalar(value)}
117                parent.append(item)
118                stack.append((indent, item))
119            else:
120                parent.append(parse_scalar(item_text))
121            continue
122        while stack and indent <= stack[-1][0]:
123            stack.pop()
124        parent = stack[-1][1]
125        if ":" not in line:
126            raise SystemExit(f"invalid yaml line: {raw_line}")
127        if not isinstance(parent, dict):
128            raise SystemExit(f"invalid yaml mapping line under list: {raw_line}")
129        key, value = line.split(":", 1)
130        key = key.strip()
131        value = value.strip()
132        if value:
133            parent[key] = parse_scalar(value)
134        else:
135            parent[key] = {}
136            last_key_at_indent[indent + 2] = (parent, key)
137            stack.append((indent, parent[key]))
138    return root

▸mock service 同时支持 HTTP 与 CLI 形态

mock 源复制在 src/real_replica_bench/cli.py:620 处理,CLI bridge 注入在 src/real_replica_bench/cli.py:632 到 src/real_replica_bench/cli.py:636,容器内 wrapper 则在 src/real_replica_bench/cli.py:760 开始安装。这说明项目把“真实工具”视为 benchmark 的一等公民。

src/real_replica_bench/cli.py · L620-636Python
620def copy_runtime_mock_sources(runtime_mocks: tuple[RuntimeMockConfig, ...], dst: Path) -> None:
621    if not runtime_mocks:
622        return
623    copyable = [mock for mock in runtime_mocks if not _runtime_mock_uses_baked_source(mock)]
624    if not copyable:
625        return
626    dst.mkdir(parents=True, exist_ok=True)
627    for mock in copyable:
628        target = dst / mock.name
629        if target.exists():
630            shutil.rmtree(target)
631        shutil.copytree(mock.source_dir, target, ignore=_runtime_mock_ignore(mock))
632    if any(mock.kind == "cli" for mock in copyable):
633        bridge_src = PROJECT_ROOT / "real_replica_bench" / "mock_services" / "bench_bridge.py"
634        if not bridge_src.is_file():
635            raise SystemExit(f"CLI runtime mocks require missing bench bridge: {bridge_src}")
636        shutil.copy2(bridge_src, dst / "bench_bridge.py")

▸奖励不是连续分数,而是全检查通过

reward.py 在 src/real_replica_bench/reward.py:118 进入 binary reward,src/real_replica_bench/reward.py:157 识别 v2 schema,src/real_replica_bench/reward.py:179 要求所有检查通过且 verifier 完成,最后在 src/real_replica_bench/reward.py:241 到 src/real_replica_bench/reward.py:243 把 reward 压成 0/1。

src/real_replica_bench/reward.py · L118-245Python
118def build_binary_final_reward(
119    reward: dict[str, Any],
120    *,
121    verifier_exit: int,
122    agent_exec_returncode: int | None,
123    agent_early_terminated: bool = False,
124) -> dict[str, Any]:
125    """Convert raw verifier output into the comparable benchmark final reward.
126
127    ``verifier/reward.json`` may keep continuous diagnostic scores. The
128    externally comparable final reward is binary: every required validation
129    check must pass, otherwise reward/score are 0.
130    """
131    final_reward = dict(reward)
132    raw_score = _score_or_none(reward.get("raw_score"))
133    raw_reward = _score_or_none(reward.get("raw_reward"))
134    if raw_score is None:
135        raw_score = _score_or_none(reward.get("score", reward.get("reward")))
136    if raw_reward is None:
137        raw_reward = _score_or_none(reward.get("reward", reward.get("score")))
138    raw_passed = bool(reward.get("passed"))
139
140    verifier_completed = verifier_exit in (0, 6)
141    verifier_passed = raw_passed and verifier_completed
142    agent_execution_passed = (
143        agent_exec_returncode in (0, None)
144        or agent_early_terminated
145        or verifier_passed
146    )
147    if agent_exec_returncode in (0, None) or agent_early_terminated:
148        agent_execution_reason = "agent process completed successfully"
149    elif verifier_passed:
150        agent_execution_reason = (
151            f"agent_exec_returncode={agent_exec_returncode}; verifier passed, "
152            "treating agent process status as diagnostic"
153        )
154    else:
155        agent_execution_reason = f"agent_exec_returncode={agent_exec_returncode}"
156
157    if _is_v2_reward_schema(reward):
158        # v2: the raw test.sh check list IS the source of truth. We do NOT
159        # add framework meta-checks (agent_execution / verifier_completed /
160        # script_verifier_or_llm) on top — those gated v1 scoring and made
161        # every report show "x/3" regardless of how many real checks the
162        # task had. Binary passed simply requires every raw check to pass
163        # AND the verifier to have completed (so the raw data is trustable).
164        # An agent crash typically leaves outputs missing and trips raw
165        # checks naturally, so we don't gate on agent_execution explicitly.
166        breakdown = reward.get("checks_breakdown")
167        checks = [
168            {
169                "id": c.get("id"),
170                "passed": bool(c.get("passed")),
171                "reason": c.get("reason"),
172                "check_type": c.get("check_type"),
173            }
174            for c in (breakdown if isinstance(breakdown, list) else [])
175            if isinstance(c, dict)
176        ]
177        passed_checks = sum(1 for c in checks if c.get("passed"))
178        total_checks = len(checks)
179        all_passed = total_checks > 0 and passed_checks == total_checks and verifier_completed
180        check_ratio = round(passed_checks / total_checks, 4) if total_checks else 0.0
181        raw_score = check_ratio
182        raw_reward = check_ratio
183        raw_passed = total_checks > 0 and passed_checks == total_checks
184        llm_check = None
185    else:
186        checks = [
187            {
188                "id": "agent_execution",
189                "passed": agent_execution_passed,
190                "reason": agent_execution_reason,
191            },
192            {
193                "id": "verifier_completed",
194                "passed": verifier_completed,
195                "reason": f"verifier_exit={verifier_exit}",
196            },
197        ]
198        llm_check = _llm_rubric_check(reward, raw_score)
199        if llm_check is not None:
200            checks.append(llm_check)
201        else:
202            checks.append(_script_verifier_check(reward, raw_score, raw_passed))
203        checks.extend(_explicit_validation_checks(reward))
204        hard_checks = reward.get("hard_checks")
205        if isinstance(hard_checks, dict) and "passable" in hard_checks:
206            checks.append(
207                {
208                    "id": "hard_checks_passable",
209                    "passed": bool(hard_checks.get("passable")),
210                    "score": hard_checks.get("score"),
211                    "reason": "; ".join(str(item) for item in hard_checks.get("cap_reasons", []) or []),
212                }
213            )
214        passed_checks = sum(1 for check in checks if check.get("passed"))
215        total_checks = len(checks)
216        all_passed = total_checks > 0 and passed_checks == total_checks
217
218    final_reward["raw_reward"] = raw_reward
219    final_reward["raw_score"] = raw_score
220    final_reward["raw_passed"] = raw_passed
221    if llm_check is not None:
222        final_reward["llm_rubric_score"] = llm_check.get("score")
223        final_reward["llm_rubric_threshold"] = llm_check.get("threshold")
224        final_reward["llm_rubric_passed"] = llm_check.get("passed")
225    final_reward["reward_mode"] = "binary_all_checks"
226    final_reward["validation_checks"] = checks
227    final_reward["check_summary"] = {
228        "total": total_checks,
229        "passed": passed_checks,
230        "failed": total_checks - passed_checks,
231        "all_passed": all_passed,
232    }
233    final_reward["framework_signals"] = {
234        "agent_execution_passed": agent_execution_passed,
235        "agent_execution_reason": agent_execution_reason,
236        "verifier_completed": verifier_completed,
237        "verifier_exit": verifier_exit,
238        "agent_exec_returncode": agent_exec_returncode,
239        "agent_early_terminated": agent_early_terminated,
240    }
241    final_reward["passed"] = all_passed
242    final_reward["reward"] = 1.0 if all_passed else 0.0
243    final_reward["score"] = final_reward["reward"]
244    final_reward.setdefault("verifier_exit", verifier_exit)
245    return final_reward
深度阅读 · Verifier 与二值奖励
§ 03 · KEY FLOW

运行流程:从 Agent 输出到可审计轨迹

▸Prompt 先声明工作目录契约

prompt 组装在 src/real_replica_bench/prompts.py:18 读取 task.md,并在 src/real_replica_bench/prompts.py:23 到 src/real_replica_bench/prompts.py:32 注入输入目录、标准工作目录和 outputs 目录。这种固定路径约定让不同 harness 的产物归档可比较。

src/real_replica_bench/prompts.py · L18-40Python
18def build_task_prompt(spec: TaskSpec) -> str:
19    prompt = (spec.case_dir / spec.entrypoint).read_text(encoding="utf-8")
20    remote_workdir = REMOTE_WORKSPACE_ROOT
21    # New layout exposes inputs under workspace/; old layout flattens them into
22    # the workdir root.
23    remote_input_dir = (
24        f"{remote_workdir}/workspace" if (spec.case_dir / "workspace").is_dir() else remote_workdir
25    )
26    remote_output_dir = f"{remote_workdir}/outputs"
27    workspace_contract = (
28        "本次工作的标准目录约定:\n"
29        f"- 任务输入目录:`{remote_input_dir}`。\n"
30        f"- 标准工作目录:`{remote_workdir}`。\n"
31        f"- 标准产物目录:`{remote_output_dir}`。\n"
32        "- 如需创建或修改任何本地文件,请在标准工作目录内完成,并把最终文件保存到标准产物目录。\n"
33        "- 不要把最终产物只保存在 `/tmp`、桌面、下载目录或其它临时路径;这些路径不会作为标准结果归档。\n\n"
34    )
35    if spec.runtime_mocks and "/mcp" in prompt:
36        workspace_contract += (
37            "MCP 工具约定:`accio-mcp-cli` 不可用;如任务要求使用本地 `/mcp` endpoint,"
38            "请直接用 `curl` 或等价命令发送 JSON-RPC `tools/list` / `tools/call` 请求。\n\n"
39        )
40    return workspace_contract + prompt

▸OpenClaw runner 明确禁止聊天式停顿

OpenClaw harness 在 src/real_replica_bench/harnesses/openclaw/runner.py:64 定义自主执行指令,并在 src/real_replica_bench/harnesses/openclaw/runner.py:78 到 src/real_replica_bench/harnesses/openclaw/runner.py:87 要求模型不要寒暄、不要反问、直接写 outputs。这是长程 unattended benchmark 的关键护栏。

src/real_replica_bench/harnesses/openclaw/runner.py · L64-87Python
64# Autonomous-agent directive, prepended to every OpenClaw prompt in the most
65# prominent position. OpenClaw's default framing is a conversational personal
66# assistant, and persona-forward models (notably GPT-5.6 Luna/Terra/Sol) latch
67# onto it — they open with a "我刚上线,你希望怎么称呼我?" self-introduction plus a
68# question, end the turn with zero tool calls, and since `openclaw agent
69# --message` is a single non-interactive invocation there is no user to answer →
70# the loop terminates and the task scores ~0. Reproduced on this runtime with
71# gpt-5.6-luna: one upstream call, zero tool calls, reward 0.
72#
73# It carries NO task-specific or verifier-facing hints, and is applied uniformly
74# to every OpenClaw run so the cross-model comparison stays fair. Because it sits
75# on the scoring boundary — changing it moves every model's starting line — it is
76# a module-level constant rather than an inline string: greppable, diffable in
77# review, and overridable in an A/B harness without editing this file.
78OPENCLAW_AGENT_DIRECTIVE = (
79    "# 角色:自主任务执行 Agent\n\n"
80    "你是一个自主运行的任务执行 agent,**不是**聊天助手。请**立即开始执行下面的任务**。\n"
81    "- **不要**自我介绍、不要给自己取名字、不要询问「如何称呼你 / 我该怎么称呼你」、不要任何寒暄开场白。\n"
82    "- 当前是**无人值守的单次执行**:没有人会回答你的任何反问,任何"
83    "「等待用户确认 / 请用户提供」都会导致任务直接失败。\n"
84    "- 直接调用工具(read / exec / browser 等)完成任务,把最终产物写到 `/task/outputs/`。"
85    "遇到不确定的地方,基于任务描述做出最合理的假设并继续推进,不要停下来等待确认。\n\n"
86    "---\n\n"
87)

▸轨迹恢复把多 harness 产物标准化

trajectory.py 从 src/real_replica_bench/trajectory.py:116 支持 Accio state,从 src/real_replica_bench/trajectory.py:227 支持 OpenClaw chat.jsonl,并用 src/real_replica_bench/trajectory.py:194 到 src/real_replica_bench/trajectory.py:203 去重工具调用、工具结果和消息。这让报告能比较不同 Agent 的操作行为。

src/real_replica_bench/trajectory.py · L116-218Python
116def recover_trajectory_from_accio_state(agent_dir: Path) -> dict[str, Any]:
117    state_dir = agent_dir / "accio_state"
118    if not state_dir.exists():
119        return {}
120    jsonl_files = sorted(state_dir.rglob("*.jsonl"))
121    tool_calls: list[Any] = []
122    tool_results: list[Any] = []
123    tool_progress: list[Any] = []
124    messages: list[dict[str, Any]] = []
125    response_chunks: list[str] = []
126    response_chunk_seen: set[str] = set()
127    usage_records: list[Any] = []
128    source_files: list[str] = []
129
130    for path in jsonl_files:
131        source_files.append(str(path.relative_to(agent_dir)))
132        for line_no, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1):
133            item = _safe_json_loads(line.strip())
134            if not isinstance(item, dict):
135                continue
136
137            raw_calls = item.get("toolCalls") or item.get("tool_calls") or []
138            if isinstance(raw_calls, list):
139                for call in raw_calls:
140                    if isinstance(call, dict):
141                        tool_calls.append({**_sanitize_tool_call(call), "_source": f"{path.relative_to(agent_dir)}:{line_no}"})
142
143            progress = item.get("toolProgress") or item.get("tool_progress")
144            for progress_item in _flatten_tool_progress(progress):
145                if isinstance(progress_item, dict):
146                    tool_progress.append({**progress_item, "_source": f"{path.relative_to(agent_dir)}:{line_no}"})
147
148            metadata = item.get("metadata")
149            if isinstance(metadata, dict) and isinstance(metadata.get("usage"), dict):
150                usage_records.append(metadata["usage"])
151
152            role = item.get("role")
153            message_type = item.get("messageType") or item.get("message_type")
154            content = item.get("content")
155            if role == "assistant" and isinstance(content, str) and content.strip():
156                chunk = content.strip()
157                if chunk not in response_chunk_seen:
158                    response_chunk_seen.add(chunk)
159                    response_chunks.append(chunk)
160            if role == "tool" or message_type == "tool_result":
161                result = {
162                    "toolCallId": item.get("toolCallId") or item.get("tool_call_id"),
163                    "toolName": item.get("name"),
164                    "content": content if isinstance(content, str) else json.dumps(content, ensure_ascii=False),
165                    "_source": f"{path.relative_to(agent_dir)}:{line_no}",
166                }
167                tool_results.append(result)
168                if isinstance(content, str) and "<task_result>" in content:
169                    chunk = content.strip()
170                    if chunk not in response_chunk_seen:
171                        response_chunk_seen.add(chunk)
172                        response_chunks.append(chunk)
173
174            if role in {"assistant", "tool", "user"} or message_type:
175                preview = content if isinstance(content, str) else ""
176                messages.append(
177                    {
178                        "role": role,
179                        "message_type": message_type,
180                        "tool_name": item.get("name"),
181                        "tool_call_count": len(raw_calls) if isinstance(raw_calls, list) else 0,
182                        "content_preview": preview[:4000],
183                        "timestamp": item.get("timestamp") or item.get("ts"),
184                        "_source": f"{path.relative_to(agent_dir)}:{line_no}",
185                    }
186                )
187
188    usage: dict[str, int] = {}
189    for record in usage_records:
190        for key, value in record.items():
191            if isinstance(value, int):
192                usage[key] = usage.get(key, 0) + value
193
194    tool_calls, duplicate_tool_calls = _dedupe_sequence(tool_calls, _tool_call_key)
195    tool_results, duplicate_tool_results = _dedupe_sequence(tool_results, _tool_result_key)
196    tool_progress, duplicate_tool_progress = _dedupe_sequence(tool_progress, _progress_key)
197    messages, duplicate_messages = _dedupe_sequence(messages, _message_key)
198    duplicate_counts = {
199        "tool_calls": duplicate_tool_calls,
200        "tool_results": duplicate_tool_results,
201        "tool_progress": duplicate_tool_progress,
202        "messages": duplicate_messages,
203    }
204
205    if not any([tool_calls, tool_results, tool_progress, messages, response_chunks]):
206        return {}
207    return {
208        "source": "accio_state",
209        "recovered_from_accio_state": True,
210        "recovered_source_files": source_files,
211        "deduplicated_recovery_items": duplicate_counts,
212        "response_text": "\n\n".join(response_chunks)[-60000:],
213        "tool_calls": tool_calls,
214        "tool_results": tool_results,
215        "tool_progress": tool_progress,
216        "messages": messages,
217        "usage": usage,
218    }
深度阅读 · OpenClaw 容器 Runner
§ 04 · TESTS

发布验证覆盖了什么

▸release validator 校验任务集合完整性

发布脚本在 scripts/validate_release.py:17 固定 107 个任务,在 scripts/validate_release.py:23 到 scripts/validate_release.py:30 固定四个 collection,并在 scripts/validate_release.py:130 到 scripts/validate_release.py:183 检查 task.toml、grader/run.sh、rubric 和 modality 切分互不重叠。

src/scripts/validate_release.py · L122-192Python
122def validate_tasks(check: Validation) -> None:
123    task_by_id: dict[str, Path] = {}
124    modality_ids: dict[str, set[str]] = {
125        "text_only": set(),
126        "browser_textcapable": set(),
127        "vision": set(),
128    }
129
130    for task_toml in sorted(DATASETS.rglob("task.toml")):
131        payload = tomllib.loads(task_toml.read_text(encoding="utf-8"))
132        task = payload.get("task") or {}
133        environment = payload.get("environment") or {}
134        task_id = task.get("id")
135        rel = task_toml.parent.relative_to(ROOT)
136        check.require(isinstance(task_id, str) and bool(task_id), f"{rel}: missing [task].id")
137        if not isinstance(task_id, str) or not task_id:
138            continue
139        check.require(task_id not in task_by_id, f"duplicate task id: {task_id}")
140        task_by_id[task_id] = task_toml.parent
141
142        for required in ("task.md", "rubric.json", "grader/run.sh"):
143            check.require((task_toml.parent / required).is_file(), f"{rel}: missing {required}")
144
145        check.require(
146            environment.get("base_images") == [PINNED_RUNTIME],
147            f"{rel}: [environment].base_images must contain only the public pinned runtime",
148        )
149
150        if bool(environment.get("requires_vision", False)):
151            modality = "vision"
152        elif bool(environment.get("requires_browser", False)):
153            modality = "browser_textcapable"
154        else:
155            modality = "text_only"
156        modality_ids[modality].add(task_id)
157
158    all_ids = load_collection(COLLECTIONS["all"])
159    check.require(
160        len(all_ids) == EXPECTED_TASK_COUNT,
161        f"full collection has {len(all_ids)} tasks; expected {EXPECTED_TASK_COUNT}",
162    )
163    check.require(len(all_ids) == len(set(all_ids)), "full collection contains duplicate task ids")
164    check.require(
165        set(all_ids) == set(task_by_id),
166        "full collection and discovered task.toml ids differ",
167    )
168
169    for modality, expected_ids in modality_ids.items():
170        actual = load_collection(COLLECTIONS[modality])
171        check.require(len(actual) == len(set(actual)), f"{modality} collection contains duplicates")
172        check.require(
173            set(actual) == expected_ids,
174            f"{modality} collection does not match task.toml modality declarations",
175        )
176
177    union = set().union(*modality_ids.values())
178    total_memberships = sum(len(items) for items in modality_ids.values())
179    check.require(union == set(all_ids), "modality collections do not cover the full collection")
180    check.require(
181        total_memberships == len(union),
182        "modality collections overlap; every task must belong to exactly one slice",
183    )
184
185    counts = {name: len(ids) for name, ids in modality_ids.items()}
186    print(
187        "tasks:"
188        f" all={len(all_ids)}"
189        f" text_only={counts['text_only']}"
190        f" browser_textcapable={counts['browser_textcapable']}"
191        f" vision={counts['vision']}"
192    )

▸Public API tests 锁住 provider wire format

测试在 tests/test_public_api.py:129 验证 Gemini native protocol,在 tests/test_public_api.py:161 验证 OpenAI Responses 多模态输入,在 tests/test_public_api.py:200 到 tests/test_public_api.py:239 锁住公开 provider config 的 API 版本和 baseUrl,避免 benchmark 发布后 wire format 漂移。

src/tests/test_public_api.py · L129-239Python
129class PublicJudgeApiTests(unittest.TestCase):
130    def test_gemini_native_protocol_and_versioned_base_url(self) -> None:
131        response = {
132            "candidates": [{"content": {"parts": [{"text": json.dumps(JUDGMENT)}]}}]
133        }
134        with _FakeEndpoint(response) as endpoint:
135            result = gemini_judge(
136                JudgeConfig(
137                    provider="gemini",
138                    model="gemini-3.1-pro-preview",
139                    api_key="test-gemini-key",
140                    base_url=f"{endpoint.base_url}/v1beta",
141                ),
142                "grade this",
143            )
144
145        self.assertTrue(result["passed"])
146        self.assertEqual(len(endpoint.requests), 1)
147        request = endpoint.requests[0]
148        self.assertEqual(
149            request["path"],
150            "/v1beta/models/gemini-3.1-pro-preview:generateContent",
151        )
152        headers = {str(k).lower(): v for k, v in request["headers"].items()}  # type: ignore[union-attr]
153        self.assertEqual(headers["x-goog-api-key"], "test-gemini-key")
154        body = request["body"]
155        self.assertEqual(body["contents"][0]["parts"][0]["text"], "grade this")  # type: ignore[index]
156        self.assertEqual(
157            body["generationConfig"]["responseMimeType"],  # type: ignore[index]
158            "application/json",
159        )
160
161    def test_openai_responses_protocol_and_multimodal_input(self) -> None:
162        with _FakeEndpoint({"output_text": json.dumps(JUDGMENT)}) as endpoint:
163            result = openai_judge(
164                JudgeConfig(
165                    provider="openai",
166                    model="test-model",
167                    api_key="test-openai-key",
168                    base_url=f"{endpoint.base_url}/v1",
169                ),
170                "grade this",
171                [
172                    {"text": "artifact image"},
173                    {
174                        "inlineData": {
175                            "mimeType": "image/png",
176                            "data": "aGVsbG8=",
177                        }
178                    },
179                ],
180            )
181
182        self.assertTrue(result["passed"])
183        request = endpoint.requests[0]
184        self.assertEqual(request["path"], "/v1/responses")
185        headers = {str(k).lower(): v for k, v in request["headers"].items()}  # type: ignore[union-attr]
186        self.assertEqual(headers["authorization"], "Bearer test-openai-key")
187        body = request["body"]
188        user_content = body["input"][1]["content"]  # type: ignore[index]
189        self.assertEqual(user_content[0], {"type": "input_text", "text": "grade this"})
190        self.assertEqual(user_content[1], {"type": "input_text", "text": "artifact image"})
191        self.assertEqual(
192            user_content[2],
193            {
194                "type": "input_image",
195                "image_url": "data:image/png;base64,aGVsbG8=",
196            },
197        )
198
199
200class PublicHarnessContractTests(unittest.TestCase):
201    def test_public_native_provider_configs_pin_api_versions(self) -> None:
202        config_root = Path(__file__).resolve().parents[1] / "configs"
203        for name in (
204            "realreplicabench_native_google_direct_models.json",
205            "realreplicabench_native_google_models.json",
206        ):
207            payload = json.loads((config_root / name).read_text(encoding="utf-8"))
208            self.assertEqual(
209                payload["providers"]["google"]["baseUrl"],
210                "https://generativelanguage.googleapis.com/v1beta",
211            )
212        qwen = json.loads(
213            (config_root / "realreplicabench_qwen37plus_native_models.json").read_text(
214                encoding="utf-8"
215            )
216        )
217        self.assertEqual(
218            qwen["providers"]["qwen"]["baseUrl"],
219            "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
220        )
221
222    def test_byo_endpoint_preset_configs_pin_wire_formats(self) -> None:
223        # Each BYO-endpoint preset is a starter template for a distinct
224        # wire format the OpenClaw runtime speaks natively. A future edit
225        # that breaks any of these contracts (wrong `api` enum, wrong
226        # default baseUrl, dropped anthropic-version header) should trip
227        # this test rather than silently ship a broken preset.
228        config_root = Path(__file__).resolve().parents[1] / "configs"
229
230        openai_chat = json.loads(
231            (config_root / "realreplicabench_openai_chat_models.json").read_text(
232                encoding="utf-8"
233            )
234        )
235        self.assertEqual(openai_chat["providers"]["openai"]["api"], "openai-completions")
236        self.assertEqual(
237            openai_chat["providers"]["openai"]["baseUrl"],
238            "https://api.openai.com/v1",
239        )
§ 05 · RISKS

亮点之外的风险

▸Docker 与 mock 复杂度是复现门槛

项目的可信度来自环境逼真,但 src/real_replica_bench/cli.py:878 之后的 mock setup、src/real_replica_bench/cli.py:906 的容器检查和 src/real_replica_bench/cli.py:949 的权限收紧都意味着本地复现比普通 benchmark 更重,CI 和用户机器需要稳定 Docker 基础设施。

▸二值奖励减少刷分空间,也降低诊断分辨率

src/real_replica_bench/reward.py:179 的 all_passed 规则很严格,适合比较最终任务完成率;但当 Agent 在 12 个检查里只错一个字段时,src/real_replica_bench/reward.py:241 到 src/real_replica_bench/reward.py:243 仍会给 0 分,因此调试阶段必须回看 checks_breakdown 和 trajectory。

最值得借鉴
公开输入 allowlist、runtime mock 隔离、trajectory 标准化。
合并前关注
任务数量持续增加时,collection 与 modality 校验是否仍覆盖全量。
Generated · 2026-08-07Source · Accio-org/RealReplicaBench